update dockerfiles based on latest streamline image

The latest images incorporate all changes previously added by the dockerfiles.
The arm image however is still missing/unreliable from the registry and must be
built independently.
This commit is contained in:
2024-04-25 10:04:06 -07:00
parent 6896f639bd
commit d26252fc11
10177 changed files with 1212770 additions and 194 deletions
+1
View File
@@ -1 +1,2 @@
build-packages
.local
+1 -51
View File
@@ -1,51 +1 @@
# FROM streamlinehealth/streamline:signalytic
# COPY my.cnf /etc/mysql/my.cnf
# COPY mysql-init.sh /var/www/html/.docker/mysql/mysql-init.sh
FROM php:8.2-fpm-alpine
# Install necessary packages and cleanup
RUN set -ex; \
apk update && \
apk add --no-cache curl gnupg mysql mysql-client pwgen && \
docker-php-ext-install pdo pdo_mysql && \
rm -rf /var/cache/apk/*
# Install Composer
RUN curl -sS https://getcomposer.org/installer | \
php -- --install-dir=/usr/bin/ --filename=composer
# Set the working directory and copy the application code
COPY streamline-src /var/www/html
COPY my.cnf /etc/mysql/my.cnf
WORKDIR /var/www/html
# Create the 'streamline' user and adjust permissions
RUN addgroup -g 1000 streamline && adduser -G streamline -g streamline -s /bin/sh -D streamline && \
composer install && \
chmod -R 777 /var/www/html/storage/ && \
chown -R streamline:streamline /var/www/html && \
mkdir -p /docker-entrypoint-initdb.d/
# Initialize MySQL
COPY streamline_initial.sql /docker-entrypoint-initdb.d/
COPY mysql-init.sh /var/www/html/.docker/mysql/mysql-init.sh
RUN mkdir /scripts && \
mkdir /scripts/pre-exec.d && \
mkdir /scripts/pre-init.d && \
chmod -R 755 /scripts
VOLUME ["/var/lib/mysql"]
# Set the startup script as executable
RUN chmod +x /var/www/html/start_up.sh
# Define the entry point and expose port 80, 3306
ENTRYPOINT ["/bin/sh", "/var/www/html/start_up.sh"]
EXPOSE 80 3306
FROM streamlinehealth/streamline:signalytic
+1 -3
View File
@@ -33,8 +33,6 @@ RUN addgroup -g 1000 streamline && adduser -G streamline -g streamline -s /bin/s
# Initialize MySQL
COPY streamline_initial.sql /docker-entrypoint-initdb.d/
COPY mysql-init.sh /var/www/html/.docker/mysql/mysql-init.sh
RUN mkdir /scripts && \
mkdir /scripts/pre-exec.d && \
mkdir /scripts/pre-init.d && \
@@ -48,4 +46,4 @@ RUN chmod +x /var/www/html/start_up.sh
# Define the entry point and expose port 80, 3306
ENTRYPOINT ["/bin/sh", "/var/www/html/start_up.sh"]
EXPOSE 80 3306
EXPOSE 80 3306
-135
View File
@@ -1,135 +0,0 @@
#!/bin/sh
# This script helps to initialise MariaDB and setup fresh database instance
# execute any pre-init scripts
for i in /scripts/pre-init.d/*sh
do
if [ -e "${i}" ]; then
echo "[i] pre-init.d - processing $i"
. "${i}"
fi
done
# create bin-logs folder
if [ ! -d "/var/lib/mysql/logs" ]; then
echo "[i] created logs folder."
mkdir -p /var/lib/mysql/logs/
fi
if [ -d "/run/mysqld" ]; then
echo "[i] mysqld already present, skipping creation"
chown -R mysql:mysql /run/mysqld
else
echo "[i] mysqld not found, creating...."
mkdir -p /run/mysqld
chown -R mysql:mysql /run/mysqld
fi
if [ -d /var/lib/mysql/mysql ]; then
echo "[i] MySQL directory already present, skipping creation"
chown -R mysql:mysql /var/lib/mysql
else
echo "[i] MySQL data directory not found, creating initial DBs"
chown -R mysql:mysql /var/lib/mysql
mysql_install_db --user=mysql --data=/var/lib/mysql > /dev/null
tfile=`mktemp`
if [ ! -f "$tfile" ]; then
return 1
fi
cat << EOF > $tfile
USE mysql;
FLUSH PRIVILEGES ;
GRANT ALL ON *.* TO 'root'@'%' identified by '$MYSQL_ROOT_PASSWORD' WITH GRANT OPTION ;
GRANT ALL ON *.* TO 'root'@'localhost' identified by '$MYSQL_ROOT_PASSWORD' WITH GRANT OPTION ;
SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ;
DROP DATABASE IF EXISTS test ;
FLUSH PRIVILEGES ;
EOF
if [[ "$MYSQL_DATABASE" != "" ]]; then
echo "[i] Creating database: $MYSQL_DATABASE"
if [ "$MYSQL_CHARSET" != "" ] && [ "$MYSQL_COLLATION" != "" ]; then
echo "[i] with character set [$MYSQL_CHARSET] and collation [$MYSQL_COLLATION]"
echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` CHARACTER SET $MYSQL_CHARSET COLLATE $MYSQL_COLLATION;" >> $tfile
else
echo "[i] with character set: 'utf8' and collation: 'utf8_general_ci'"
echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` CHARACTER SET utf8 COLLATE utf8_general_ci;" >> $tfile
fi
if [ "$MYSQL_USER" != "" ]; then
echo "[i] Creating user: $MYSQL_USER with password $MYSQL_PASSWORD"
echo "GRANT ALL ON `$MYSQL_DATABASE`.* TO '$MYSQL_USER'@'127.0.0.1' IDENTIFIED BY '$MYSQL_PASSWORD';" >> $tfile;
echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* to '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" >> $tfile
fi
fi
/usr/bin/mysqld --user=mysql --bootstrap --verbose=0 --skip-name-resolve --skip-networking=0 < $tfile
# /usr/bin/mysqld --user=mysql --bootstrap --verbose=0 --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin < $tfile
rm -f $tfile
# only run if we have a starting MYSQL_DATABASE env variable AND
# the /docker-entrypoint-initdb.d/ file is not empty
if [ "$MYSQL_DATABASE" != "" ] && [ "$(ls -A /docker-entrypoint-initdb.d 2>/dev/null)" ]; then
# start the server temporarily so that we can import seed files
echo
echo "Preparing to process the contents of /docker-entrypoint-initdb.d/"
echo
TEMP_OUTPUT_LOG=/tmp/mysqld_output
/usr/bin/mysqld --user=mysql --skip-name-resolve --skip-networking=0 --silent-startup > "${TEMP_OUTPUT_LOG}" 2>&1 &
# /usr/bin/mysqld --user=mysql --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin --silent-startup > "${TEMP_OUTPUT_LOG}" 2>&1 &
PID="$!"
# watch the output log until the server is running
until tail "${TEMP_OUTPUT_LOG}" | grep -q "Version:"; do
sleep 0.2
done
# use mysql client to import seed files while temp db is running
# use the starting MYSQL_DATABASE so mysql knows where to import
MYSQL_CLIENT="/usr/bin/mysql -u root -p$MYSQL_ROOT_PASSWORD"
# loop through all the files in the seed directory
# redirect input (<) from .sql files into the mysql client command line
# pipe (|) the output of using `gunzip -c` on .sql.gz files
for f in /docker-entrypoint-initdb.d/*; do
case "$f" in
*.sql) echo " $0: running $f"; eval "${MYSQL_CLIENT} ${MYSQL_DATABASE} < $f"; echo ;;
*.sql.gz) echo " $0: running $f"; gunzip -c "$f" | eval "${MYSQL_CLIENT} ${MYSQL_DATABASE}"; echo ;;
esac
done
# send the temporary mysqld server a shutdown signal
# and wait till it's done before completeing the init process
kill -s TERM "${PID}"
wait "${PID}"
rm -f TEMP_OUTPUT_LOG
echo "Completed processing seed files."
fi;
echo
echo 'MySQL init process done. Ready for start up.'
echo
echo "exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0" "$@"
# echo "exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin " "$@"
fi
# execute any pre-exec scripts
for i in /scripts/pre-exec.d/*sh
do
if [ -e "${i}" ]; then
echo "[i] pre-exec.d - processing $i"
. ${i}
fi
done
exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 &
return $?
@@ -69,7 +69,8 @@ EOF
fi
fi
/usr/bin/mysqld --user=mysql --bootstrap --verbose=0 --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin < $tfile
/usr/bin/mysqld --user=mysql --bootstrap --verbose=0 --skip-name-resolve --skip-networking=0 < $tfile
# /usr/bin/mysqld --user=mysql --bootstrap --verbose=0 --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin < $tfile
rm -f $tfile
# only run if we have a starting MYSQL_DATABASE env variable AND
@@ -81,7 +82,8 @@ EOF
echo "Preparing to process the contents of /docker-entrypoint-initdb.d/"
echo
TEMP_OUTPUT_LOG=/tmp/mysqld_output
/usr/bin/mysqld --user=mysql --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin --silent-startup > "${TEMP_OUTPUT_LOG}" 2>&1 &
/usr/bin/mysqld --user=mysql --skip-name-resolve --skip-networking=0 --silent-startup > "${TEMP_OUTPUT_LOG}" 2>&1 &
# /usr/bin/mysqld --user=mysql --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin --silent-startup > "${TEMP_OUTPUT_LOG}" 2>&1 &
PID="$!"
# watch the output log until the server is running
@@ -115,7 +117,8 @@ EOF
echo 'MySQL init process done. Ready for start up.'
echo
echo "exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin " "$@"
echo "exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0" "$@"
# echo "exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin " "$@"
fi
# execute any pre-exec scripts
@@ -127,6 +130,6 @@ do
fi
done
exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 --log-basename=bin --log-bin=/var/lib/mysql/logs/bin &
exec /usr/bin/mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 &
return $?
@@ -5,4 +5,5 @@ images
insurance_members
hospital_resources
csv_imports
users
users/*
!users/placeholder.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,5 @@
vendor/
tests/Fixtures.php
.idea/
/.phpunit.result.cache
.phpunit.cache/
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Africa's Talking
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,246 @@
# Africa's Talking PHP SDK
[![Latest Stable Version](https://img.shields.io/packagist/v/africastalking/africastalking)](https://packagist.org/packages/africastalking/africastalking)
> This SDK provides convenient access to the Africa's Talking API for applications written in PHP.
## Documentation
Take a look at the [API docs here](https://developers.africastalking.com).
## Install
You can install the PHP SDK via composer or by downloading the source
#### Via Composer
The recommended way to install the SDK is with [Composer](http://getcomposer.org/).
```bash
composer require africastalking/africastalking
```
## Usage
The SDK needs to be instantiated using your username and API key, which you can get from the [dashboard](https://account.africastalking.com).
> You can use this SDK for either production or sandbox apps. For sandbox, the app username is **ALWAYS** `sandbox`
```php
use AfricasTalking\SDK\AfricasTalking;
$username = 'YOUR_USERNAME'; // use 'sandbox' for development in the test environment
$apiKey = 'YOUR_API_KEY'; // use your sandbox app API key for development in the test environment
$AT = new AfricasTalking($username, $apiKey);
// Get one of the services
$sms = $AT->sms();
// Use the service
$result = $sms->send([
'to' => '+2XXYYYOOO',
'message' => 'Hello World!'
]);
print_r($result);
```
See [example](example/) for more usage examples.
## Instantiation
Instantiating the class will give you an object with available methods
- `$AT = new AfricasTalking($username, $apiKey)`: Instantiate the class
- Get available service
- [SMS Service](#sms): `$sms = $AT->sms()`
- [Content Service](#content): `$content = $AT->content()`
- [Airtime Service](#airtime): `$airtime = $AT->airtime()`
- [Mobile Data Service](#mobiledata): `$mobileData = $AT->mobileData()`
- [Voice Service](#voice): `$voice = $AT->voice()`
- [Token Service](#token): `$token = $AT->token()`
- [Application Service](#application): `$application = $AT->application()`
### Application
- `fetchApplicationData()`: Get app information. e.g balance
### Airtime
- `send($parameters, $options)`: Send airtime
- **$parameters:** associative array with the following keys:
- `recipients`: An array of arrays containing the following keys
- `phoneNumber`: Recipient of airtime. `REQUIRED`
- `currencyCode`: 3-digit ISO format currency code (e.g `KES`, `USD`, `UGX` etc). `REQUIRED`
- `amount`: Amount to send. `REQUIRED`
- **$options:** optional associative array with the following keys:
- `idempotencyKey`: Key to use when making idempotent requests
- `maxNumRetry`: Maximum number of retries in case of failed airtime deliveries due to telco unavailability or any other reason.
### SMS
- `send($options)`: Send a message
- `message`: SMS content. `REQUIRED`
- `to`: An array of phone numbers. `REQUIRED`
- `from`: Shortcode or alphanumeric ID that is registered with your Africa's Talking account.
- `enqueue`: Set to `true` if you would like to deliver as many messages to the API without waiting for an acknowledgement from telcos.
- `fetchMessages($options)`: Fetch your messages
- `lastReceivedId`: This is the id of the message you last processed. Defaults to `0`
***The followoing methods have been moved to the content service, but, have been maintained on SMS for backwards compatibility:***
- `sendPremium($options)`: Send a premium SMS. Calls `$content->send($options)`
- `createSubscription($options)`: Create a premium subscription. Calls `$content->createSubscription($options)`
- `fetchSubscriptions($options)`: Fetch your premium subscription data. Calls `$content->fetchSubscriptions($options)`
- `deleteSubscription($options)`: Delete a phone number from a premium subscription. Calls `$content->$deleteSubscription($options)`
### Content
- `send($options)`: Send a premium SMS
- `message`: SMS content. `REQUIRED`
- `to`: An array of phone numbers. `REQUIRED`
- `from`: Shortcode that is registered with your Africa's Talking account. `REQUIRED`
- `keyword`: Your premium product keyword
- `linkId`: "[...] We forward the `linkId` to your application when a user sends a message to your onDemand service"
- `retryDurationInHours`: "This specifies the number of hours your subscription message should be retried in case it's not delivered to the subscriber"
- `createSubscription($options)`: Create a premium subscription
- `shortCode`: Premium short code mapped to your account. `REQUIRED`
- `keyword`: Premium keyword under the above short code and is also mapped to your account. `REQUIRED`
- `phoneNumber`: PhoneNumber to be subscribed `REQUIRED`
- `fetchSubscriptions($options)`: Fetch your premium subscription data
- `shortCode`: Premium short code mapped to your account. `REQUIRED`
- `keyword`: Premium keyword under the above short code and mapped to your account. `REQUIRED`
- `lastReceivedId`: ID of the subscription you believe to be your last. Defaults to `0`
- `deleteSubscription($options)`: Delete a phone number from a premium subscription
- `shortCode`: Premium short code mapped to your account. `REQUIRED`
- `keyword`: Premium keyword under the above short code and is also mapped to your account. `REQUIRED`
- `phoneNumber`: PhoneNumber to be subscribed `REQUIRED`
### Mobile Data
- `send($parameters, $options)`: Send mobile data to customers
- **$parameters:** associative array with the following keys:
- `productName`: Payment product on Africa's Talking. `REQUIRED`
- `recipients`: A list of recipients. Each recipient has:
- `phoneNumber`: Customer phone number (in international format). `REQUIRED`
- `quantity`: Mobile data amount. `REQUIRED`
- `unit`: Mobile data unit. Can either be `MB` or `GB`. `REQUIRED`
- `validity`: How long the mobile data is valid for. Must be one of `Day`, `Week` and `Month`. `REQUIRED`
- `metadata`: Additional data to associate with the tranasction. `REQUIRED`
- `isPromoBundle`: This is an optional field that can be either `true` or `false`.
- **$options:** optional associative array with the following keys:
- `idempotencyKey`: Key to use when making idempotent requests
- `findTransaction($parameters)`: Find a particular transaction
- `transactionId`: ID of trancation to find. `REQUIRED`
- `fetchWalletBalance()`: Fetch your payment wallet balance
### Voice
- `call($options)`: Initiate a phone call
- `to`: Phone number that you wish to dial (in international format). `REQUIRED`
- `from`: Phone number on Africa's Talking (in international format). `REQUIRED`
- `clientRequestId`: Variable sent to your Events Callback URL that can be used to tag the call. `OPTIONAL`
- `fetchQueuedCalls($options)`: Fetch queued calls on a phone number
- `phoneNumber`: Phone number mapped to your Africa's Talking account (in international format). `REQUIRED`
- `name`: Fetch calls for a specific queue.
- `uploadMediaFile($options)`: Upload a voice media file
- `phoneNumber`: phone number mapped to your Africa's Talking account (in international format). `REQUIRED`
- `url`: The url of the file to upload. Should start with `http(s)://`. `REQUIRED`
#### MessageBuilder
Build voice xml when callback URL receives a POST from the voice API. Actions can be chained to create an XML string.
```php
$voiceActions = $voice->messageBuilder();
$xmlresponse = $voiceActions
->getDigits($options)
->say($text)
->record()
->build();
```
- `say($text)`: Add a `Say` action
- `text`: Text (in English) that will be read out to the user.
- `play($url)`: Add a `Play` action
- `url`: Public url to an audio file. This file will be played back to user.
- `getDigits($options)`: Add a `GetDigits` action
- `numDigits`: Number of digits should be gotten from the user
- `timeout`: Timeout (in seconds) for getting digits from a user.
- `finishOnKey`: key which will terminate the action of getting digits.
- `callbackUrl`: URL to forward the results of the GetDigits action.
- `dial($options)`: Add a `Dial` action
- `phoneNumbers`: An array of phone numbers (in international format) to call. `REQUIRED`
- `record`: Boolean - Whether to record the conversation.
- `sequenntial`: Boolean - If many numbers provided for `phoneNumbers`, determines whether the phone numbers will be dialed one after the other or at the same time.
- `callerId`: Africa's Talking number you want to dial out with.
- `ringBackTone`: URL location of a media playback you would want the user to listen to when the call has been placed before its picked up.
- `maxDuration`: maximum amount of time in seconds a call should take.
- `conference()`: Add a `Conference` action
- `record($options)`: Add a `Record` action
- `finishOnKey`: Key which will terminate the action of recording.
- `maxLength`: Maximum amount of time in seconds a recording should take.
- `timeout`: Timeout (in seconds) for getting a recording from a user.
- `trimSilence`: Boolean - Specifies whether you want to remove the initial and final parts of a recording where user was silent.
- `playBeep`: Boolean - Specifies whether the API should play a beep when recording starts.
- `callbackUrl`: URL to forward the results of the Recording action.
- `enqueue($options)`: Add an `Enqueue` action
- `holdMusic`: URL to the file to be played while the user is on hold.
- `name`: Name of queue to put call on.
- `deqeue($options)`: Add a `Dequeue` acton
- `phoneNumber`: Phone number mapped to your Africa's Talking account which a user called to join the queue. `REQUIRED`
- `name`: Name of queue you want to dequeue from.
- `reject()`: Add a `Reject` action
- `redirect($url)`: Add a `Redirect` action
- `url`: URL to transfer control of the call to
- `build()`: Build the xml after chaining some of the above actions
### Token
- `generateAuthToken()`: Generate an auth token to use for authenticating API requests instead of your API key.
## Testing the SDK
The SDK uses [PHPUnit](https://phpunit.de/manual/current/en/index.html) as the test runner.
To run available tests, from the root of the project run:
```bash
# Configure needed fixtures, e.g sandbox api key, Africa's Talking products
cp tests/Fixtures.php.tpl tests/Fixtures.php
# Run tests
phpunit --testdox
```
## Issues
If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/AfricasTalkingLtd/africastalking-php/issues).
@@ -0,0 +1,28 @@
{
"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/",
"AfricasTalking\\SDK\\Tests\\": "tests"
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
# Example
**Run**
First, make sure to put your sandbox api key in `index.php`, then:
```bash
$ composer install
$ php -S localhost:9090
$ # open http://localhost:9090
```
@@ -0,0 +1,9 @@
{
"name": "africastalking-example",
"description": "Simple SDK usage",
"require": {
"php": ">=5.3.0",
"africastalking/africastalking": "dev-develop",
"altorouter/altorouter": "1.1.0"
}
}
@@ -0,0 +1,50 @@
<?php
require 'vendor/autoload.php';
use AfricasTalking\SDK\AfricasTalking;
$username = "sandbox";
$apiKey = getenv("API_KEY");
$AT = new AfricasTalking($username, $apiKey);
// Router
$router = new AltoRouter();
$router->map( 'GET', '/', function() {
require __DIR__ . '/views/index.php';
});
$router->map( 'POST', '/auth/register/[*:phone]', function ($phone) {
global $AT;
$sms = $AT->sms();
$response = $sms->send(array(
"to" => $phone,
"from" => "AT2FA",
"message" => "Welcome to Awesome Company",
));
header("Content-Type: application/json; charset=UTF-8");
echo json_encode($response);
});
$router->map( 'POST', '/airtime/[*:phone]', function ($phone) {
global $AT;
$airtime = $AT->airtime();
$response = $airtime->send(array(
"recipients" => array(
array(
"phoneNumber" => $phone,
"amount" => $_GET['amount'],
)
)
));
header("Content-Type: application/json; charset=UTF-8");
echo json_encode($response);
});
$match = $router->match();
if( $match && is_callable( $match['target'] ) ) {
call_user_func_array( $match['target'], $match['params'] );
} else {
header( $_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
}
@@ -0,0 +1,75 @@
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-right: 15px;
padding-left: 15px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
}
/* Customize container */
@media (min-width: 768px) {
/*.container {
max-width: 1200px;
}*/
}
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
/*.jumbotron .btn {
padding: 14px 24px;
font-size: 21px;
}*/
/* Supporting marketing content */
.marketing {
margin: 40px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-right: 0;
padding-left: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
pre {
background: white;
}
@@ -0,0 +1,123 @@
"use strict";
$(function () {
function log(message) {
$("#response").text(message);
$('pre span').each(function(i, block) {
hljs.highlightBlock(block);
});
}
$("#signUp").click(() => {
let phone = $("#phone").val();
if (!phone) {
log(JSON.stringify({ error: "Enter a phone number"}, null, 2));
return;
}
log("Sending SMS...");
$.ajax({
type: "POST",
url: `/auth/register/${phone}`,
success: (resp) => {
try {
log(JSON.stringify(resp, null, 2));
} catch (ex) {
log(resp);
}
}
});
});
$("#airtime").click(() => {
const phone = $("#phone").val();
const amount = $("#amount").val();
if (!phone) {
log(JSON.stringify({ error: "Enter a phone number"}, null, 2));
return;
}
if (!amount) {
log(JSON.stringify({ error: "Enter an amount (with currency) e,g, KES 334"}, null, 2));
return;
}
log("Sending Airtime...");
$.ajax({
type: "POST",
url: `/airtime/${phone}?amount=${amount}`,
success: (resp) => {
try {
log(JSON.stringify(resp, null, 2));
} catch (ex) {
log(resp);
}
}
});
});
$("#mobileCheckout").click(() => {
const phone = $("#phone").val();
const amount = $("#mobileCheckoutAmount").val();
if (!phone) {
log(JSON.stringify({ error: "Enter a phone number"}, null, 2));
return;
}
if (!amount) {
log(JSON.stringify({ error: "Enter an amount (with currency) e,g, KES 334"}, null, 2));
return;
}
log("Initiating Mobile Checkout...");
$.ajax({
type: "POST",
url: `/mobile/checkout/${phone}?amount=${amount}`,
success: (resp) => {
try {
log(JSON.stringify(resp, null, 2));
} catch (ex) {
log(resp);
}
}
});
});
$("#mobileB2C").click(() => {
const phone = $("#phone").val();
const amount = $("#mobileB2CAmount").val();
if (!phone) {
log(JSON.stringify({ error: "Enter a phone number"}, null, 2));
return;
}
if (!amount) {
log(JSON.stringify({ error: "Enter an amount (with currency) e,g, KES 334"}, null, 2));
return;
}
log("Initiating Mobile B2C...");
$.ajax({
type: "POST",
url: `/mobile/b2c/${phone}?amount=${amount}`,
success: (resp) => {
try {
log(JSON.stringify(resp, null, 2));
} catch (ex) {
log(resp);
}
}
});
});
});
@@ -0,0 +1,60 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Africa's Talking</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/paper/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/tomorrow.min.css" />
<link href="public/css/style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active"><a href="#">Home</a></li>
<li role="presentation"><a href="http://docs.africastalking.com" target="_blank">Docs</a></li>
</ul>
</nav>
<h3 class="text-muted">Africa's Talking</h3>
</div>
<br/>
<br/>
<div class="col-md-6">
<pre><span id="response"></span></pre>
</div>
<div class="col-md-6">
<p class="lead">Try clicking some buttons</p>
<div class="col-sm-6 col-offset-sm-3">
<input id="phone" type="text" class="form-control" placeholder="Your phone number">
</div>
<p><a class="btn btn-success btn-sm" role="button" id="signUp">Send SMS</a></p>
<div class="col-sm-6 col-offset-sm-3">
<input id="amount" type="text" class="form-control" placeholder="Amount e.g. USD 35">
</div>
<p><a class="btn btn-success btn-sm" role="button" id="airtime">Airtime</a></p>
<div class="col-sm-6 col-offset-sm-3">
<input id="mobileCheckoutAmount" type="text" class="form-control" placeholder="Amount e.g. KES 4456">
</div>
<p><a class="btn btn-success btn-sm" role="button" id="mobileCheckout">Mobile Checkout</a></p>
<div class="col-sm-6 col-offset-sm-3">
<input id="mobileB2CAmount" type="text" class="form-control" placeholder="Amount e.g. KES 4456">
</div>
<p><a class="btn btn-success btn-sm" role="button" id="mobileB2C">Mobile B2C</a></p>
</div>
</div> <!-- /container -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/xml.min.js"></script>
<script src="public/js/main.js"></script>
</body>
</html>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="./vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="AfricasTalking Test Suite">
<directory suffix="Test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
@@ -0,0 +1,134 @@
<?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 $voiceClient;
protected $tokenClient;
protected $contentClient;
protected $mobileDataClient;
public $baseUrl;
protected $voiceUrl;
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,62 @@
<?php
namespace AfricasTalking\SDK;
class Airtime extends Service
{
public function send($parameters, $options = [])
{
if(empty($parameters['recipients'])) {
return $this->error("recipients must be specified");
}
if(!is_array($parameters['recipients'])) {
return $this->error("recipients must be an array");
}
foreach ($parameters['recipients'] as $key=>$recipient){
if(!is_array($recipient)) {
return $this->error("every recipient must be an array");
}
if(!array_key_exists('phoneNumber', $recipient) || !array_key_exists('currencyCode', $recipient) || !array_key_exists('amount', $recipient)) {
return $this->error("phoneNumber, currencyCode and amount must be specified for each recipient");
}
$currencyCode = $recipient['currencyCode'];
if (strlen($currencyCode) != 3) {
return $this->error('currencyCode must be in 3-digit ISO format');
}
$recipient['amount'] = $recipient['currencyCode'].' '.$recipient['amount'];
unset($recipient['currencyCode']);
$parameters['recipients'][$key] = $recipient;
}
$data = [
'username' => $this->username,
'recipients' => json_encode($parameters['recipients'])
];
if (isset($options['maxNumRetry']) && is_numeric($options['maxNumRetry']) && $options['maxNumRetry'] > 0) {
$data['maxNumRetry'] = $options['maxNumRetry'];
}
$requestOptions = [
'form_params' => $data,
];
if(isset($options['idempotencyKey'])) {
$requestOptions['headers'] = [
'Idempotency-Key' => $options['idempotencyKey'],
];
}
$response = $this->client->post('airtime/send', $requestOptions);
return $this->success($response);
}
}
@@ -0,0 +1,17 @@
<?php
namespace AfricasTalking\SDK;
class Application extends Service
{
public function doFetchApplication()
{
$response = $this->client->get('user', ['query' => ['username'=> $this->username]]);
return $this->success($response);
}
public function fetchApplicationData()
{
return $this->doFetchApplication();
}
}
@@ -0,0 +1,122 @@
<?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']
];
$response = $this->client->post('subscription/create', ['form_params' => $data ] );
return $this->success($response);
}
public function deleteSubscription ($options)
{
if (empty($options['phoneNumber']) ||
empty($options['shortCode']) ||
empty($options['keyword'])) {
return $this->error("phoneNumber, shortCode and keyword must be specified");
}
$data = [
'username' => $this->username,
'phoneNumber' => $options['phoneNumber'],
'shortCode' => $options['shortCode'],
'keyword' => $options['keyword']
];
$response = $this->client->post('subscription/delete', ['form_params' => $data ] );
return $this->success($response);
}
public function fetchSubscriptions($options)
{
if(empty($options['shortCode']) || empty($options['keyword'])) {
return $this->error("shortCode and keyword must be specified");
}
if (empty($options['lastReceivedId'])) {
$options['lastReceivedId'] = 0;
}
if (!is_numeric($options['lastReceivedId'])) {
return $this->error('lastReceivedId must be an integer');
}
$data = [
'username' => $this->username,
'lastReceivedId' => $options['lastReceivedId'],
'shortCode' => $options['shortCode'],
'keyword' => $options['keyword']
];
$response = $this->client->get('subscription', ['query' => $data ] );
return $this->success($response);
}
}
@@ -0,0 +1,108 @@
<?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,
];
if (isset($parameters['isPromoBundles'])) {
$requestData['isPromoBundle'] = $parameters['isPromoBundles'];
}
$requestOptions = [
'json' => $requestData,
];
if(isset($options['idempotencyKey'])) {
$requestOptions['headers'] = [
'Idempotency-Key' => $options['idempotencyKey'],
];
}
$response = $this->client->post('mobile/data/request', $requestOptions);
return $this->success($response);
}
protected function doFindTransaction($options)
{
if (!isset($options['transactionId'])) {
return $this->error('transactionId must be defined');
}
$requestData = [
'username' => $this->username,
'transactionId' => $options['transactionId']
];
$response = $this->client->get('query/transaction/find', ['query' => $requestData]);
return $this->success($response);
}
protected function doFetchWalletBalance()
{
$requestData = [
'username' => $this->username
];
$response = $this->client->get('query/wallet/balance', ['query' => $requestData]);
return $this->success($response);
}
}
@@ -0,0 +1,82 @@
<?php
namespace AfricasTalking\SDK;
class SMS extends Service
{
protected $content;
public function __construct($client, $username, $apiKey, $content)
{
parent::__construct($client, $username, $apiKey);
$this->content = $content;
}
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'])) {
$data['from'] = $options['from'];
}
$response = $this->client->post('messaging', ['form_params' => $data ]);
return $this->success($response);
}
public function fetchMessages($options = [])
{
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']
];
$response = $this->client->get('messaging', ['query' => $data ] );
return $this->success($response);
}
public function sendPremium($options)
{
return $this->content->send($options);
}
public function createSubscription ($options)
{
return $this->content->createSubscription($options);
}
public function deleteSubscription ($options)
{
return $this->content->deleteSubscription($options);
}
public function fetchSubscriptions($options)
{
return $this->content->fetchSubscriptions($options);
}
}
@@ -0,0 +1,33 @@
<?php
namespace AfricasTalking\SDK;
abstract class Service
{
protected $client;
protected $username;
protected $apiKey;
public function __construct($client, $username, $apiKey)
{
$this->client = $client;
$this->username = $username;
$this->apiKey = $apiKey;
}
protected static function error($data) {
return [
'status' => 'error',
'data' => $data
];
}
protected static function success($data) {
return [
'status' => 'success',
'data' => json_decode($data->getBody()->getContents())
];
}
}
@@ -0,0 +1,13 @@
<?php
namespace AfricasTalking\SDK;
class Token extends Service
{
public function generateAuthToken()
{
$requestData = json_encode(['username' => $this->username]);
$response = $this->client->post('auth-token/generate', ['body' => $requestData ] );
return $this->success($response);
}
}
@@ -0,0 +1,508 @@
<?php
namespace AfricasTalking\SDK;
class Voice extends Service
{
private $xmlString;
public function __call($method, $args)
{
// First check if method exists
if (method_exists($this, 'build' . $method)) {
if (!isset($args[0])) {
$args = [ 0 => ''];
}
return $this->stringBuilder('build'. $method, $args[0]);
} else if (method_exists($this, 'do' . $method)) {
if (!isset($args[0])) {
$args = [ 0 => ''];
}
return $this->apiCall('do' . $method, $args[0]);
} else {
return $this->error($method .' is an invalid Voice SDK Method');
}
}
public function messageBuilder()
{
return $this;
}
private function stringBuilder($method, $args)
{
$result = $this->$method($args);
if (empty($this->xmlString)) {
$this->xmlString = '<?xml version="1.0" encoding="UTF-8"?><Response>'. $result;
} else {
$this->xmlString .= $result;
}
return $this;
}
private function apiCall($method, $args)
{
return $this->$method($args);
}
/**
* Builds XML string from chained voice actions
*
* @return string
*/
public function build()
{
if (empty($this->xmlString)) {
return null;
}
return $this->xmlString . '</Response>';
}
protected function doCall($options)
{
if (!isset($options['to']) || !isset($options['from'])) {
return $this->error('The parameters to and from must be defined');
}
// Validate callTo
$checkCallTo = strpos($options['to'], '+');
if ($checkCallTo === false || $checkCallTo !== 0) {
return $this->error('callTo must be in the format \'+2XXYYYYYYYYY\'');
}
// Validate callFrom
$checkCallFrom = strpos($options['from'], '+');
if ($checkCallFrom === false || $checkCallFrom !== 0) {
return $this->error('callFrom must be in the format \'+2XXYYYYYYYYY\'');
}
$requestData = [
'username' => $this->username,
'to' => $options['to'],
'from' => $options['from']
];
if (isset($options['clientRequestId']) && !empty($options['clientRequestId'])) {
$requestData['clientRequestId'] = (string) $options['clientRequestId'];
}
$response = $this->client->post('call', ['form_params' => $requestData ] );
return $this->success($response);
}
protected function doUploadMediaFile($options)
{
// Check and validate phoneNumber
if (!isset($options['phoneNumber'])) {
return $this->error('Phone number is required and must be in the format \'+2XXYYYYYYYYY\'');
}
$phoneNumber = $options['phoneNumber'];
$checkPhoneNumber = strpos($phoneNumber, '+');
if ($checkPhoneNumber === false || $checkPhoneNumber != 0) {
return $this->error('Phone number must be in the format \'+2XXYYYYYYYYY\'');
}
if (!isset($options['url'])) {
return $this->error('url must be defined');
}
$url = $options['url'];
// Check if valid URL passed
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
return $this->error('URL not valid');
}
$requestData = [
'username' => $this->username,
'phoneNumber' => $phoneNumber,
'url' => $url
];
$response = $this->client->post('mediaUpload', ['form_params' => $requestData]);
return $this->success($response);
}
protected function dofetchQueuedCalls($options)
{
// Check and validate phoneNumber
if (!isset($options['phoneNumber'])) {
return $this->error('Phone number is required and must be in the format \'+2XXYYYYYYYYY\'');
}
$phoneNumber = $options['phoneNumber'];
$checkPhoneNumber = strpos($phoneNumber, '+');
if ($checkPhoneNumber === false || $checkPhoneNumber != 0) {
return $this->error('Phone number must be in the format \'+2XXYYYYYYYYY\'');
}
$requestData = [
'username' => $this->username,
'phoneNumbers' => $phoneNumber
];
if(isset($options['name'])) {
$requestData['name'] = $options['name'];
}
$response = $this->client->post('queueStatus', ['form_params' => $requestData]);
return $this->success($response);
}
protected function buildSay($options)
{
if (is_string($options)) {
return "<Say>$options</Say>";
}
// Check for text
if (!isset($options['text'])) {
return $this->error('Please set text to be read out');
}
$text = $options['text'];
// Check if read out voice has been set
if (isset($options['voice'])) {
$voice = $options['voice'];
}
// Check if playBeep option has been set
if (isset($options['playBeep'])) {
$playBeep = $options['playBeep'];
}
if (isset($options['voice']) && isset($options['playBeep'])) {
$sayString = '<Say voice="' . $voice . '" playBeep="'. $playBeep .'">'. $text .'</Say>';
} else if (isset($options['voice'])) {
$sayString = '<Say voice="' . $voice . '">'. $text .'</Say>';
} else if (isset($options['playBeep'])) {
$sayString = '<Say playBeep="'. $playBeep .'">'. $text .'</Say>';
} else {
$sayString = "<Say>$text</Say>";
}
return $sayString;
}
protected function buildPlay($url)
{
if (!$this->isValidURL($url))
return $this->error('Play URL is not valid');
$playString = '<Play url="'. $url . '"/>';
return $playString;
}
protected function buildGetDigits($options) {
// Check for text
if (!isset($options['text'])) {
return $this->error('Please set text to be read out');
}
$text = $options['text'];
// Check if URL is set
if (isset($options['url'])) {
$url = $options['url'];
}
// Get number of digits
if(isset($options['numDigits'])) {
$numDigits = $options['numDigits'];
if (!is_numeric($numDigits)) {
return $this->error('Please set a number value for the timeout');
}
}
// Get timeout
if(isset($options['timeout'])) {
$timeout = $options['timeout'];
if (!is_numeric($timeout)) {
return $this->error('Please set a number value for the timeout');
}
}
// Get finishOnKey
$finishOnKey = $options['finishOnKey'];
// Get callbackURL
if (isset($options['callBackUrl'])) {
$callBackUrl = $options['callBackUrl'];
if (!$this->isValidURL($callBackUrl)) {
return $this->error('Please set a valid callback URL');
}
}
// -- NOW TO BUILD STRING
// Build opening tag
$getDigitsString = '<GetDigits ';
if (!empty($finishOnKey)) {
$getDigitsString .= ' finishOnKey="'. $finishOnKey .'"';
}
if (!empty($timeout)) {
$getDigitsString .= ' timeout="'. $timeout .'"';
}
if (!empty($numDigits)) {
$getDigitsString .= ' numDigits="'. $numDigits .'"';
}
if (!empty($callBackUrl)) {
$getDigitsString .= ' callBackUrl="'. $callBackUrl .'"';
}
$getDigitsString .= '>';
// ... add child element
if (!empty($text)) {
$getDigitsString .= $this->buildSay($text);
}
if (!empty($url)) {
$getDigitsString .= $this->buildPlay($url);
}
$getDigitsString .= '</GetDigits>';
return $getDigitsString;
}
protected function buildDial($options)
{
// Validate phoneNumber
if (!isset($options['phoneNumbers'])) {
return $this->error('Please specifiy at least one number to dial');
}
$phoneNumbers = implode(",", $options["phoneNumbers"]);
// Check if ringback tone is set
if (isset($options['ringbackTone'])) {
if (!$this->isValidURL($options['ringbackTone'])) {
return $this->error('ringbackTone not a valid URL');
}
$ringbackTone = $options['ringbackTone'];
}
// Check if record is set
if (!isset($options['record']) || !is_bool($options['record'])) {
$record = false;
} else {
$record = $options['record'];
}
// change record to true or false string
$record ? $record = "true" : $record ="false";
// Check if sequential
if (!isset($options['sequential']) || !is_bool($options['sequential'])) {
$sequential = false;
} else {
$sequential = $options['sequential'];
}
// change sequential to true or false string
$sequential ? $sequential = "true" : $sequential ="false";
// Check if callerId is set
if (!isset($options['callerId']) || !($options['callerId'])) {
$callerId = false;
} else {
$callerId = $options['callerId'];
}
// change callerId to number provided or false string
$callerId ? $callerId = $callerId : $callerId ="false";
// Check if maxDuration is set
if(isset($options['maxDuration'])) {
$maxDuration = $options['maxDuration'];
if (!is_integer($maxDuration) || $maxDuration < 0) {
return $this->error('Max duration must be an integer value');
}
}
$dialString = '<Dial phoneNumbers="'. $phoneNumbers . '"';
if (!empty($record)) {
$dialString .= ' record="'. $record .'"';
}
if (!empty($sequential)) {
$dialString .= ' sequential="'. $sequential .'"';
}
if (!empty($callerId)) {
$dialString .= ' callerId="'. $callerId .'"';
}
if (!empty($ringbackTone)) {
$dialString .= ' ringbackTone="'. $ringbackTone .'"';
}
if (!empty($maxDuration)) {
$dialString .= ' maxDuration="'. $maxDuration .'"';
}
$dialString .= ' />';
return $dialString;
}
protected function buildRecord($options)
{
/** Terminal Recording **/
if (empty($options)) {
return '<Record />';
}
/** Partial Recording **/
// Get finishOnKey
$finishOnKey = $options['finishOnKey'];
// Get Max Length
if (isset($options['maxLength'])) {
$maxLength = $options['maxLength'];
if (!is_numeric($maxLength)) {
return $this->error('Please set a number value for the timeout');
}
}
// Get timeout
if (isset($options['timeout'])) {
$timeout = $options['timeout'];
if (!is_numeric($timeout)) {
return $this->error('Please set a number value for the timeout');
}
}
// Check if trimSilence is set
if (!isset($options['trimSilence']) || !is_bool($options['trimSilence'])) {
$trimSilence = false;
} else {
$trimSilence = $options['trimSilence'];
}
// change trimSilence to true or false string
$trimSilence ? $trimSilence = "true" : $trimSilence ="false";
// Check if playBeep option has been set
if (!isset($options['playBeep']) || !is_bool($options['playBeep'])) {
$playBeep = false;
} else {
$playBeep = $options['playBeep'];
}
// change playBeep to true or false string
$playBeep ? $playBeep = "true" : $playBeep ="false";
// Get callbackURL
if (isset($options['callBackUrl'])) {
$callBackUrl = $options['callBackUrl'];
if (!$this->isValidURL($callBackUrl)) {
return $this->error('Please set a valid callback URL');
}
}
// Build opening tag
$recordString = '<Record';
if (!empty($finishOnKey)) {
$recordString .= ' finishOnKey="'. $finishOnKey .'"';
}
if (!empty($maxLength)) {
$recordString .= ' maxLength="'. $maxLength .'"';
}
if (!empty($timeout)) {
$recordString .= ' timeout="'. $timeout .'"';
}
if (!empty($trimSilence)) {
$recordString .= ' trimSilence="'. $trimSilence .'"';
}
if (!empty($playBeep)) {
$playBeep .= ' playBeep="'. $playBeep .'"';
}
if (!empty($callBackUrl)) {
$getDigitsString .= ' callBackUrl="'. $callBackUrl .'"';
}
$recordString .= ' />';
return $recordString;
}
protected function buildEnqueue($options)
{
// Check if holdMusic option has been set
if (isset($options['holdMusic'])) {
$holdMusic = $options['holdMusic'];
if (!$this->isValidURL($holdMusic)) {
return $this->error('Please set a valid URL value for holdMusic');
}
}
// Build opening tag
$enqueueString = '<Enqueue';
if (!empty($holdMusic)) {
$enqueueString .= ' holdMusic="'. $holdMusic .'"';
}
if (isset($options['name'])) {
$name = $options['name'];
$enqueueString .= ' name="'. $name .'"';
}
$enqueueString .= ' />';
return $enqueueString;
}
protected function buildDequeue($options)
{
// Check if holdMusic option has been set
if (!isset($options['phoneNumber'])) {
return $this->error('Please enter a valid phone number');
}
$phoneNumber = $options['phoneNumber'];
// Build opening tag
$dequeueString = '<Dequeue phoneNumber="'. $phoneNumber . '"';
if (isset($options['name'])) {
$name = $options['name'];
$dequeueString .= ' name="'. $name .'"';
}
$dequeueString .= ' />';
return $dequeueString;
}
protected function buildConference()
{
return '<Conference />';
}
protected function buildRedirect($url)
{
return '<Redirect>'. $url.'</Redirect>';
}
protected function buildReject($url)
{
return '<Reject />';
}
private function isValidURL($url)
{
// If no url passed then it can't be valid
if (empty($url)) {
return false;
}
return !filter_var($url, FILTER_VALIDATE_URL) === false;
}
}
@@ -0,0 +1,46 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
class AfricasTalkingTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$this->client = new AfricasTalking($this->username, $this->apiKey);
}
public function testSMSClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\SMS::class, $this->client->sms());
}
public function testContentClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Content::class, $this->client->content());
}
public function testAirtimeClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Airtime::class, $this->client->airtime());
}
public function testVoiceClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Voice::class, $this->client->voice());
}
public function testApplicationClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\Application::class, $this->client->application());
}
public function testMobileDataClass()
{
$this->assertInstanceOf(\AfricasTalking\SDK\MobileData::class, $this->client->mobileData());
}
}
@@ -0,0 +1,63 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
class AirtimeTest extends \PHPUnit\Framework\TestCase
{
public function setup(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->airtime();
}
public function testSendAirtimeToOne()
{
$response = $this->client->send([
'recipients' => [[
'phoneNumber' => Fixtures::$phoneNumber,
'currencyCode' => Fixtures::$currencyCode,
'amount' => Fixtures::$amount
]]
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
public function testSendAirtimeIdempotency()
{
$response = $this->client->send([
'recipients' => [[
'phoneNumber' => Fixtures::$phoneNumber,
'currencyCode' => Fixtures::$currencyCode,
'amount' => Fixtures::$amount
]]
], [
'idempotencyKey' => 'req-' . mt_rand(10, 100),
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
public function testSendAirtimeToMany()
{
$response = $this->client->send([
'recipients' => [[
'phoneNumber' => Fixtures::$phoneNumber,
'currencyCode' => Fixtures::$currencyCode,
'amount' => Fixtures::$amount
], [
'phoneNumber' => '+2347038151149',
'currencyCode' => 'NGN',
'amount' => '10000'
]]
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
}
@@ -0,0 +1,24 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
class ApplicationTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->application();
}
public function testFetchAplication()
{
$response = $this->client->fetchApplicationData();
$this->assertObjectHasProperty('UserData', $response['data']);
}
}
@@ -0,0 +1,66 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
class ContentTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->content();
$this->tokenClient = $at->token();
}
public function send()
{
$response = $this->client->send([
'to' => Fixtures::$multiplePhoneNumbersSMS,
'linkId' => 'messageLinkId',
'keyword' => Fixtures::$keyword,
'from' => Fixtures::$shortCode,
'message' => 'Testing Premium...'
]);
$this->assertObjectHasProperty('SMSMessageData', $response['data']);
}
public function testCreateSubscription()
{
$response = $this->client->createSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword,
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('success',$response['status']);
}
public function testDeleteSubscription()
{
$response = $this->client->deleteSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('success',$response['status']);
}
public function testFetchSubscriptions()
{
$response = $this->client->fetchSubscriptions([
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
}
@@ -0,0 +1,39 @@
<?php
namespace AfricasTalking\SDK\Tests;
class Fixtures
{
public static $username = 'sandbox';
public static $apiKey = '';
public static $dateOfBirth = '';
public static $accountName = 'Test Bank Account';
public static $accountNumber = '0123456789';
public static $phoneNumber = '+254724486439';
public static $multiplePhoneNumbersSMS = ['+254724486439', '+254724567567', '+254724567569'];
public static $voicePhoneNumber = '+254711082489';
public static $voicePhoneNumber2 = '+254724486439';
public static $narration = 'Test Payment';
public static $shortCode = '';
public static $keyword = 'Test';
public static $alphanumeric = 'Test';
public static $targetProductCode = 1411;
public static $mediaUrl = 'http://thesoundeffect.com/music/mp3/AintTooProudToBeg.mp3';
public static $amount = '60';
public static $currencyCode = 'KES';
public static $currencyCode2 = 'NGN';
public static $productName = 'TestProduct';
public static $provider = 'ATHENA';
public static $transferType = '';
public static $startDate = '2018-01-01';
public static $endDate = '2018-12-30';
public static $transactionId = 'ATPid_SampleTxnId1';
public static $otp = '1234';
public static $MobileDataRecipients = array([
'phoneNumber' => '25471xxxxxxx',
'quantity' => 15,
'unit' => 'MB',
'validity' => 'Day',
'metadata' => ['notes' => 'Data for January 2018']
]);
}
@@ -0,0 +1,43 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use AfricasTalking\SDK\MobileData;
use GuzzleHttp\Exception\GuzzleException;
class MobileDataTest 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->mobileData();
}
public function testSend()
{
$response = $this->client->send([
'productName' => Fixtures::$productName,
'recipients' => Fixtures::$MobileDataRecipients,
]);
$this->assertArrayHasKey('status', $response);
}
public function testFindTransaction()
{
$response = $this->client->findTransaction([
'transactionId' => Fixtures::$transactionId
]);
$this->assertEquals('Failure', $response['data']->status);
}
public function testFetchWalletBalance()
{
$response = $this->client->fetchWalletBalance();
$this->assertEquals('Success', $response['data']->status);
}
}
@@ -0,0 +1,135 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
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->assertEquals('success',$response['status']);
}
public function testDeleteSubscription()
{
$response = $this->client->deleteSubscription([
'phoneNumber' => Fixtures::$phoneNumber,
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('success',$response['status']);
}
public function testFetchSubscriptions()
{
$response = $this->client->fetchSubscriptions([
'shortCode' => Fixtures::$shortCode,
'keyword' => Fixtures::$keyword
]);
$this->assertObjectHasProperty('responses', $response['data']);
}
}
@@ -0,0 +1,24 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
class TokenTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->token();
}
public function testGenerateAuthToken()
{
$response = $this->client->generateAuthToken();
$this->assertEquals(3600, $response['data']->lifetimeInSeconds);
}
}
@@ -0,0 +1,90 @@
<?php
namespace AfricasTalking\SDK\Tests;
use AfricasTalking\SDK\AfricasTalking;
use GuzzleHttp\Exception\GuzzleException;
class VoiceTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
$this->username = Fixtures::$username;
$this->apiKey = Fixtures::$apiKey;
$at = new AfricasTalking($this->username, $this->apiKey);
$this->client = $at->voice();
}
public function testCall()
{
$response = $this->client->call([
'from' => Fixtures::$voicePhoneNumber,
'to' => Fixtures::$voicePhoneNumber2
]);
$this->assertObjectHasProperty('entries', $response['data']);
}
public function testCallsMustHaveRequiredAttributes()
{
$response = $this->client->call([
'from' => Fixtures::$voicePhoneNumber
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testFetchQueuedCalls()
{
$response = $this->client->fetchQueuedCalls([
'phoneNumber' => Fixtures::$voicePhoneNumber,
'name' => 'someQueueName'
]);
$this->assertArrayHasKey('status', $response);
}
public function testFetchQueuedCallsMustHaveRequiredAttributes()
{
$response = $this->client->fetchQueuedCalls();
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testUploadMediaFile()
{
$response = $this->client->uploadMediaFile([
'phoneNumber' => Fixtures::$voicePhoneNumber,
'url' => Fixtures::$mediaUrl
]);
$this->assertArrayHasKey('status', $response);
}
public function testuploadMediaFileMustHaveRequiredAttributes()
{
$response = $this->client->uploadMediaFile([
'url' => 'test@google'
]);
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
public function testuploadMediaFileCannotBeEmpty()
{
$response = $this->client->uploadMediaFile();
$this->assertArrayHasKey('status',$response);
$this->assertEquals('error',$response['status']);
}
// public function testMessageBuilder()
// {
// // TODO
// }
}
+25
View File
@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit379a36e429894b61b55afc3b84fa116a::getLoader();
@@ -0,0 +1,15 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,50 @@
{
"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",
"illuminate/filesystem": "^9|^10",
"knplabs/knp-snappy": "^1.4"
},
"require-dev": {
"orchestra/testbench": "^7|^8"
},
"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.
|
| Timout:
|
| The amount of time to wait (in seconds) before PDF / Image generation is stopped.
| Setting this to false disables the timeout (unlimited processing time).
|
| Options:
|
| The wkhtmltopdf command options. These are passed directly to wkhtmltopdf.
| See https://wkhtmltopdf.org/usage/wkhtmltopdf.txt for all options.
|
| Env:
|
| The environment variables to set while running the wkhtmltopdf process.
|
*/
'pdf' => [
'enabled' => true,
'binary' => env('WKHTML_PDF_BINARY', '/usr/local/bin/wkhtmltopdf'),
'timeout' => false,
'options' => [],
'env' => [],
],
'image' => [
'enabled' => true,
'binary' => env('WKHTML_IMG_BINARY', '/usr/local/bin/wkhtmltoimage'),
'timeout' => false,
'options' => [],
'env' => [],
],
];
@@ -0,0 +1,209 @@
## Snappy PDF/Image Wrapper for Laravel
[![Tests](https://github.com/barryvdh/laravel-snappy/workflows/Tests/badge.svg)](https://github.com/barryvdh/laravel-snappy/actions)
[![Packagist License](https://poser.pugx.org/barryvdh/laravel-snappy/license.png)](http://choosealicense.com/licenses/mit/)
[![Latest Stable Version](https://poser.pugx.org/barryvdh/laravel-snappy/version.png)](https://packagist.org/packages/barryvdh/laravel-snappy)
[![Total Downloads](https://poser.pugx.org/barryvdh/laravel-snappy/d/total.png)](https://packagist.org/packages/barryvdh/laravel-snappy)
[![Fruitcake](https://img.shields.io/badge/Powered%20By-Fruitcake-b2bc35.svg)](https://fruitcake.nl/)
This package is a ServiceProvider for Snappy: [https://github.com/KnpLabs/snappy](https://github.com/KnpLabs/snappy).
### Wkhtmltopdf Installation
Choose one of the following options to install wkhtmltopdf/wkhtmltoimage.
1. Download wkhtmltopdf from [here](http://wkhtmltopdf.org/downloads.html);
2. Or install as a composer dependency. See [wkhtmltopdf binary as composer dependencies](https://github.com/KnpLabs/snappy#wkhtmltopdf-binary-as-composer-dependencies) for more information.
#### Attention! Please note that some dependencies (libXrender for example) may not be present on your system and may require manual installation.
### Testing the wkhtmltopdf installation
After installing you should be able to run wkhtmltopdf from the command line / shell.
If you went for the second option the binaries will be at `/vendor/h4cc/wkhtmltoimage-amd64/bin` and `/vendor/h4cc/wkhtmltopdf-amd64/bin`.
#### Attention vagrant users!
Move the binaries to a path that is not in a synced folder, for example:
```bash
cp vendor/h4cc/wkhtmltoimage-amd64/bin/wkhtmltoimage-amd64 /usr/local/bin/
cp vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64 /usr/local/bin/
```
and make it executable:
```bash
chmod +x /usr/local/bin/wkhtmltoimage-amd64
chmod +x /usr/local/bin/wkhtmltopdf-amd64
```
This will prevent the error 126.
### Package Installation
Require this package in your composer.json and update composer.
```bash
composer require barryvdh/laravel-snappy
```
### Laravel
**Laravel 5.5** uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider/Facade. If you also use laravel-dompdf, watch out for conflicts. It could be better to manually register the Facade.
After updating composer, add the ServiceProvider to the providers array in config/app.php
```php
Barryvdh\Snappy\ServiceProvider::class,
```
Optionally you can use the Facade for shorter code. Add this to your facades:
```php
'PDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'SnappyImage' => Barryvdh\Snappy\Facades\SnappyImage::class,
```
Finally you can publish the config file:
```bash
php artisan vendor:publish --provider="Barryvdh\Snappy\ServiceProvider"
```
### Snappy config file
The main change to this config file (config/snappy.php) will be the path to the binaries.
For example, when loaded with composer, the line should look like:
```php
'binary' => base_path('vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64'),
```
If you followed the vagrant steps, the line should look like:
```php
'binary' => '/usr/local/bin/wkhtmltopdf-amd64',
```
For windows users you'll have to add double quotes to the bin path for wkhtmltopdf:
```php
'binary' => '"C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf"'
```
### Lumen
In `bootstrap/app.php` add:
```php
class_alias('Barryvdh\Snappy\Facades\SnappyPdf', 'PDF');
$app->register(Barryvdh\Snappy\LumenServiceProvider::class);
```
Optionally, add the facades like so:
```php
class_alias(Barryvdh\Snappy\Facades\SnappyPdf::class, 'PDF');
class_alias(Barryvdh\Snappy\Facades\SnappyImage::class, 'SnappyImage');
```
To customise the configuration file, copy the file `/vendor/barryvdh/laravel-snappy/config/snappy.php` to the `/config` folder.
### Usage
You can create a new Snappy PDF/Image instance and load a HTML string, file or view name. You can save it to a file, or inline (show in browser) or download.
Using the App container:
```php
$snappy = App::make('snappy.pdf');
//To file
$html = '<h1>Bill</h1><p>You owe me money, dude.</p>';
$snappy->generateFromHtml($html, '/tmp/bill-123.pdf');
$snappy->generate('http://www.github.com', '/tmp/github.pdf');
//Or output:
return new Response(
$snappy->getOutputFromHtml($html),
200,
array(
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="file.pdf"'
)
);
```
Using the wrapper:
```php
$pdf = App::make('snappy.pdf.wrapper');
$pdf->loadHTML('<h1>Test</h1>');
return $pdf->inline();
```
Or use the facade:
```php
$pdf = PDF::loadView('pdf.invoice', $data);
return $pdf->download('invoice.pdf');
```
You can chain the methods:
```php
return PDF::loadFile('http://www.github.com')->inline('github.pdf');
```
You can change the orientation and paper size
```php
PDF::loadHTML($html)->setPaper('a4')->setOrientation('landscape')->setOption('margin-bottom', 0)->save('myfile.pdf')
```
If you need the output as a string, you can get the rendered PDF with the output() function, so you can save/output it yourself.
See the [wkhtmltopdf manual](http://wkhtmltopdf.org/usage/wkhtmltopdf.txt) for more information/settings.
### Testing - PDF fake
As an alternative to mocking, you may use the `PDF` facade's `fake` method. When using fakes, assertions are made after the code under test is executed:
```php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use PDF;
class ExampleTest extends TestCase
{
public function testPrintOrderShipping()
{
PDF::fake();
// Perform order shipping...
PDF::assertViewIs('view-pdf-order-shipping');
PDF::assertSee('Name');
}
}
```
#### Other available assertions:
```php
PDF::assertViewIs($value);
PDF::assertViewHas($key, $value = null);
PDF::assertViewHasAll(array $bindings);
PDF::assertViewMissing($key);
PDF::assertSee($value);
PDF::assertSeeText($value);
PDF::assertDontSee($value);
PDF::assertDontSeeText($value);
PDF::assertFileNameIs($value);
```
### License
This Snappy Wrapper for Laravel is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)
@@ -0,0 +1,16 @@
<?php
namespace Barryvdh\Snappy\Facades;
use Illuminate\Support\Facades\Facade as BaseFacade;
class SnappyImage extends BaseFacade {
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor() { return 'snappy.image.wrapper'; }
}
@@ -0,0 +1,40 @@
<?php
namespace Barryvdh\Snappy\Facades;
use Illuminate\Support\Facades\Facade as BaseFacade;
use Barryvdh\Snappy\PdfFaker;
/**
* @method static \Barryvdh\Snappy\PdfWrapper setPaper($paper, $orientation = 'portrait')
* @method static \Barryvdh\Snappy\PdfWrapper setWarnings($warnings)
* @method static \Barryvdh\Snappy\PdfWrapper setOptions(array $options)
* @method static \Barryvdh\Snappy\PdfWrapper loadView($view, $data = array(), $mergeData = array(), $encoding = null)
* @method static \Barryvdh\Snappy\PdfWrapper loadHTML($string, $encoding = null)
* @method static \Barryvdh\Snappy\PdfWrapper loadFile($file)
* @method static string output($options = [])
* @method static \Barryvdh\Snappy\PdfWrapper save()
* @method static \Illuminate\Http\Response download($filename = 'document.pdf')
* @method static \Illuminate\Http\Response inline($filename = 'document.pdf')
*
*/
class SnappyPdf extends BaseFacade {
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor() { return 'snappy.pdf.wrapper'; }
/**
* Replace the bound instance with a fake.
*
* @return void
*/
public static function fake()
{
static::swap(new PdfFaker(app('snappy.pdf')));
}
}
@@ -0,0 +1,104 @@
<?php namespace Barryvdh\Snappy;
use Knp\Snappy\Image;
use Illuminate\Filesystem\Filesystem;
class IlluminateSnappyImage extends Image {
/**
* @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,108 @@
<?php namespace Barryvdh\Snappy;
use Knp\Snappy\Pdf;
use Illuminate\Filesystem\Filesystem;
class IlluminateSnappyPdf extends Pdf {
/**
* @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 strlen($filename) <= PHP_MAXPATHLEN && $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,199 @@
<?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();
/**
* @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);
}
}
@@ -0,0 +1,81 @@
<?php namespace Barryvdh\Snappy;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
class LumenServiceProvider extends BaseServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->configure('snappy');
$configPath = __DIR__ . '/../config/snappy.php';
$this->mergeConfigFrom($configPath, 'snappy');
}
public function boot()
{
if ($this->app['config']->get('snappy.pdf.enabled')) {
$this->app->bind('snappy.pdf',function ($app) {
$binary = $app['config']->get('snappy.pdf.binary', '/usr/local/bin/wkhtmltopdf');
$options = $app['config']->get('snappy.pdf.options', array());
$env = $app['config']->get('snappy.pdf.env', array());
$timeout = $app['config']->get('snappy.pdf.timeout', false);
$snappy = new IlluminateSnappyPdf($app['files'], $binary, $options, $env);
if (false !== $timeout) {
$snappy->setTimeout($timeout);
}
return $snappy;
});
$this->app->bind('snappy.pdf.wrapper',function ($app) {
return new PdfWrapper($app['snappy.pdf']);
});
$this->app->alias('snappy.pdf.wrapper', 'Barryvdh\Snappy\PdfWrapper');
}
if ($this->app['config']->get('snappy.image.enabled')) {
$this->app->bind('snappy.image',function ($app) {
$binary = $app['config']->get('snappy.image.binary', '/usr/local/bin/wkhtmltoimage');
$options = $app['config']->get('snappy.image.options', array());
$env = $app['config']->get('snappy.image.env', array());
$timeout = $app['config']->get('snappy.image.timeout', false);
$image = new IlluminateSnappyImage($app['files'], $binary, $options, $env);
if (false !== $timeout) {
$image->setTimeout($timeout);
}
return $image;
});
$this->app->bind('snappy.image.wrapper',function ($app) {
return new ImageWrapper($app['snappy.image']);
});
$this->app->alias('snappy.image.wrapper', 'Barryvdh\Snappy\ImageWrapper');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return array('snappy.pdf', 'snappy.pdf.wrapper', 'snappy.image', 'snappy.image.wrapper');
}
}
@@ -0,0 +1,297 @@
<?php namespace Barryvdh\Snappy;
use Illuminate\View\View;
use PHPUnit\Framework\Assert as PHPUnit;
use Illuminate\Support\Facades\View as ViewFacade;
class PdfFaker extends PdfWrapper
{
protected $view;
protected $filename;
/**
* Load a View and convert to HTML
*
* @param string $view
* @param array $data
* @param array $mergeData
* @return $this
*/
public function loadView($view, $data = array(), $mergeData = array())
{
$this->view = ViewFacade::make($view, $data, $mergeData);
return parent::loadView($view, $data, $mergeData);
}
/**
* Ensure that the response has a view as its original content.
*
* @return $this
*/
protected function ensureResponseHasView()
{
if (! isset($this->view) || ! $this->view instanceof View) {
return PHPUnit::fail('The response is not a view.');
}
return $this;
}
public function assertViewIs($value)
{
PHPUnit::assertEquals($value, $this->view->getName());
return $this;
}
/**
* Assert that the response view has a given piece of bound data.
*
* @param string|array $key
* @param mixed $value
* @return $this
*/
public function assertViewHas($key, $value = null)
{
if (is_array($key)) {
return $this->assertViewHasAll($key);
}
$this->ensureResponseHasView();
if (is_null($value)) {
PHPUnit::assertArrayHasKey($key, $this->view->getData());
} elseif ($value instanceof \Closure) {
PHPUnit::assertTrue($value($this->view->$key));
} else {
PHPUnit::assertEquals($value, $this->view->$key);
}
return $this;
}
/**
* Assert that the response view has a given list of bound data.
*
* @param array $bindings
* @return $this
*/
public function assertViewHasAll(array $bindings)
{
foreach ($bindings as $key => $value) {
if (is_int($key)) {
$this->assertViewHas($value);
} else {
$this->assertViewHas($key, $value);
}
}
return $this;
}
/**
* Assert that the response view is missing a piece of bound data.
*
* @param string $key
* @return $this
*/
public function assertViewMissing($key)
{
$this->ensureResponseHasView();
PHPUnit::assertArrayNotHasKey($key, $this->view->getData());
return $this;
}
/**
* Assert that the given string is contained within the response.
*
* @param string $value
* @return $this
*/
public function assertSee($value)
{
PHPUnit::assertStringContainsString($value, $this->html);
return $this;
}
/**
* Assert that the given string is contained within the response text.
*
* @param string $value
* @return $this
*/
public function assertSeeText($value)
{
PHPUnit::assertStringContainsString($value, strip_tags($this->html));
return $this;
}
/**
* Assert that the given string is not contained within the response.
*
* @param string $value
* @return $this
*/
public function assertDontSee($value)
{
PHPUnit::assertStringNotContainsString($value, $this->html);
return $this;
}
/**
* Assert that the given string is not contained within the response text.
*
* @param string $value
* @return $this
*/
public function assertDontSeeText($value)
{
PHPUnit::assertStringNotContainsString($value, strip_tags($this->html));
return $this;
}
/**
* Assert that the given string is equal to the saved filename.
*
* @param string $value
* @return $this
*/
public function assertFileNameIs($value)
{
PHPUnit::assertEquals($value, $this->filename);
return $this;
}
public function output()
{
return '%PDF-1.3
%?????????
4 0 obj
<< /Length 5 0 R /Filter /FlateDecode >>
stream
x+T(T0BSKS=#
C=
K??T?p?<}?bC??bC0,N??5?34???05j1?7)N?
z/?
endstream
endobj
5 0 obj
73
endobj
2 0 obj
<< /Type /Page /Parent 3 0 R /Resources 6 0 R /Contents 4 0 R /MediaBox [0 0 595.28 841.89]
>>
endobj
6 0 obj
<< /ProcSet [ /PDF ] /ColorSpace << /Cs1 7 0 R >> >>
endobj
8 0 obj
<< /Length 9 0 R /N 3 /Alternate /DeviceRGB /Filter /FlateDecode >>
stream
x??wTS??Ͻ7??" %?z ?;HQ?I?P??&vDF)VdT?G?"cE
??b? ?P??QDE?݌k ?5?ޚ??Y?????g?}׺P???tX?4?X???\???X??ffG?D???=???HƳ??.?d??,?P&s???"7C$
E?6<~&??S??2????)2?12? ??"?įl???+?ɘ?&?Y??4???Pޚ%ᣌ?\?%?g?|e?TI???(????L0?_??&?l?2E???9?r??9h?x?g??Ib?טi???f??S?b1+??M?xL???
?0??o?E%Ym?h?????Y??h????~S?=?z?U?&?ϞA??Y?l?/??$Z????U?m@??O? ??ޜ??l^???
\'
???ls?k.+?7???oʿ?9?????V;???#I3eE妧?KD??
??d?????9i???,?????UQ? ??h??<?X?.d
???6\'~?khu_}?9P?I?o=C#$n?z}?[1
Ⱦ?h???s?2z???
\?n?LA"S??
?dr%?,?߄l??t?
?3p? ??H?.Hi@?A>?
p1?v?jpԁz?N?6p\W?
?G@
???ٰG???Dx????J?>???,?_@?FDB?X$!k?"??E????H?q???a???Y??bVa?bJ0՘c?VL?6f3????bձ?X\'??&?x?*???s?b|!??`[????a?;???p~?\2n5??׌????
ߏƿ\'? Zk?!? $l$???4Q??Ot"?y?\b)???A?I&N?I?$R$)???TIj"]&=&?!??:dGrY@^O?$? _%??P?n?X????ZO?D}J}/G?3???ɭ???k??{%O?חw?_.?\'_!J????Q?@?S???V?F??=?IE???b?b?b?b??5?Q%?????O?@??%?!BӥyҸ?M?:?e?0G7??ӓ????? e%e[?(????R?0`?3R????????4?????6?i^??)??*n*|?"?f????LUo?՝?m?O?0j&jaj?j??.??ϧ?w?ϝ_4????갺?z??j???=???U?4?5?n?ɚ??4ǴhZ
?Z?Z?^0????Tf%??9?????-?>?ݫ=?c??Xg?N??]?.[7A?\?SwBOK/X/_?Q?>Q?????G?[??? ?`?A???????a?a??c#????*?Z?;?8c?q??>?[&???I?I??MS???T`?ϴ?
k?h&4?5?Ǣ??YY?F֠9?<?|?y??+
=?X???_,?,S-?,Y)YXm?????Ěk]c}džj?c?Φ?浭?-?v??};?]???N????"?&?1=?x????tv(??}???????\'{\'??I?ߝY?Σ?
??-r?q?r?.d.?_xp??Uە?Z???M׍?v?m???=????+K?G?ǔ????
^???W?W????b?j?>:>?>?>?v??}/?a??v?????????O8? ?
?FV>
2 u?????/?_$\?B?Cv?< 5
]?s.,4?&?y?Ux~xw-bEDCĻH????G??KwF?G?E?GME{E?EK?X,Y??F?Z? ?=$vr????K????
??.3\????r???Ϯ?_?Yq*??©?L??_?w?ד??????+??]?e???????D??]?cI?II?OA??u?_?䩔???)3?ѩ?i?????B%a??+]3=\'?/?4?0C??i??U?@ёL(sYf????L?H?$?%?Y
?j??gGe??Q?????n?????~5f5wug?v????5?k??֮\۹Nw]??????m mH???Fˍen???Q?Q??`h????B?BQ?-?[l?ll??f??jۗ"^??b???O%ܒ??Y}W???????????w?w????X?bY^?Ю?]?????W?Va[q`i?d??2???J?jGէ??????{?????׿?m???>???Pk?Am?a?????꺿g_D?H??G?G??u?;??7?7?6?Ʊ?q?o???C{??P3???8!9?????
<?y?}??\'?????Z?Z???։??6i{L{??ӝ?-???|??????gKϑ???9?w~?Bƅ??:Wt>???ҝ????ˁ??^?r?۽??U??g?9];}?}????????_?~i??m??p???㭎?}??]?/???}?????.?{?^?=?}????^??z8?h?c??\'
O*????????f?????`ϳ?g???C/????O?ϩ?+F?F?G?Gό???z????ˌ??ㅿ)????ѫ?~w??gb???k???Jި?9???m?d???wi獵?ޫ???????c?Ǒ??O?O????w| ??x&mf??????
endstream
endobj
9 0 obj
2612
endobj
7 0 obj
[ /ICCBased 8 0 R ]
endobj
3 0 obj
<< /Type /Pages /MediaBox [0 0 595.28 841.89] /Count 1 /Kids [ 2 0 R ] >>
endobj
10 0 obj
<< /Type /Catalog /Pages 3 0 R >>
endobj
11 0 obj
(Leeg)
endobj
12 0 obj
(Mac OS X 10.13.3 Quartz PDFContext)
endobj
13 0 obj
(Pages)
endobj
14 0 obj
(D:20180330091154Z00\'00\')
endobj
1 0 obj
<< /Title 11 0 R /Producer 12 0 R /Creator 13 0 R /CreationDate 14 0 R /ModDate
14 0 R >>
endobj
xref
0 15
0000000000 65535 f
0000003414 00000 n
0000000187 00000 n
0000003133 00000 n
0000000022 00000 n
0000000169 00000 n
0000000297 00000 n
0000003098 00000 n
0000000365 00000 n
0000003078 00000 n
0000003222 00000 n
0000003272 00000 n
0000003295 00000 n
0000003348 00000 n
0000003372 00000 n
trailer
<< /Size 15 /Root 10 0 R /Info 1 0 R /ID [ <ea5d2c625a19688ce1ff05174d8a0261>
<ea5d2c625a19688ce1ff05174d8a0261> ] >>
startxref
3519
%%EOF';
}
/**
* Save the PDF to a file
*
* @param $filename
* @return $this
*/
public function save($filename, $overwrite = false)
{
$this->filename = $filename;
return $this;
}
}
@@ -0,0 +1,285 @@
<?php namespace Barryvdh\Snappy;
use Illuminate\Http\Response;
use Knp\Snappy\Pdf as SnappyPDF;
use Illuminate\Support\Facades\View;
use Illuminate\Contracts\Support\Renderable;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* A Laravel wrapper for SnappyPDF
*
* @package laravel-snappy
* @author Barry vd. Heuvel
*/
class PdfWrapper{
/**
* @var \Knp\Snappy\Pdf
*/
protected $snappy;
/**
* @var array
*/
protected $options = array();
/**
* @var string
*/
protected $html;
/**
* @var string
*/
protected $file;
/**
* @param \Knp\Snappy\Pdf $snappy
*/
public function __construct(SnappyPDF $snappy)
{
$this->snappy = $snappy;
}
/**
* Get the Snappy instance.
*
* @return \Knp\Snappy\Pdf
*/
public function snappy()
{
return $this->snappy;
}
/**
* Set temporary folder
*
* @param string $path
*/
public function setTemporaryFolder($path)
{
$this->snappy->setTemporaryFolder($path);
return $this;
}
/**
* Set the paper size (default A4)
*
* @param string $paper
* @param string $orientation
* @return $this
*/
public function setPaper($paper, $orientation=null)
{
$this->snappy->setOption('page-size', $paper);
if($orientation){
$this->snappy->setOption('orientation', $orientation);
}
return $this;
}
/**
* Set the orientation (default portrait)
*
* @param string $orientation
* @return $this
*/
public function setOrientation($orientation)
{
$this->snappy->setOption('orientation', $orientation);
return $this;
}
/**
* Show or hide warnings
*
* @param bool $warnings
* @return $this
* @deprecated
*/
public function setWarnings($warnings)
{
//Doesn't do anything
return $this;
}
/**
* @param string $name
* @param mixed $value
* @return $this
*/
public function setOption($name, $value)
{
if ($value instanceof Renderable) {
$value = $value->render();
}
$this->snappy->setOption($name, $value);
return $this;
}
/**
* @param array $options
* @return $this
*/
public function setOptions($options)
{
$this->snappy->setOptions($options);
return $this;
}
/**
* Load a HTML string
*
* @param Array|string|Renderable $html
* @return $this
*/
public function loadHTML($html)
{
if ($html instanceof Renderable) {
$html = $html->render();
}
$this->html = $html;
$this->file = null;
return $this;
}
/**
* Load a HTML file
*
* @param string $file
* @return $this
*/
public function loadFile($file)
{
$this->html = null;
$this->file = $file;
return $this;
}
/**
* Load a View and convert to HTML
*
* @param string $view
* @param array $data
* @param array $mergeData
* @return $this
*/
public function loadView($view, $data = array(), $mergeData = array())
{
$view = View::make($view, $data, $mergeData);
return $this->loadHTML($view);
}
/**
* 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('PDF Generator requires a html or file in order to produce output.');
}
/**
* Save the PDF to a file
*
* @param $filename
* @return $this
*/
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 PDF downloadable by the user
*
* @param string $filename
* @return \Illuminate\Http\Response
*/
public function download($filename = 'document.pdf')
{
return new Response($this->output(), 200, array(
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="'.$filename.'"'
));
}
/**
* Return a response with the PDF to show in the browser
*
* @param string $filename
* @return \Illuminate\Http\Response
*/
public function inline($filename = 'document.pdf')
{
return new Response($this->output(), 200, array(
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
));
}
/**
* Return a response with the PDF to show in the browser
*
* @param string $filename
* @return \Symfony\Component\HttpFoundation\StreamedResponse
* @deprecated use inline() instead
*/
public function stream($filename = 'document.pdf')
{
return new StreamedResponse(function() {
echo $this->output();
}, 200, array(
'Content-Type' => 'application/pdf',
'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);
}
}
@@ -0,0 +1,88 @@
<?php namespace Barryvdh\Snappy;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
class ServiceProvider extends BaseServiceProvider {
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$configPath = __DIR__ . '/../config/snappy.php';
$this->mergeConfigFrom($configPath, 'snappy');
}
public function boot()
{
$configPath = __DIR__ . '/../config/snappy.php';
$this->publishes([$configPath => config_path('snappy.php')], 'config');
if($this->app['config']->get('snappy.pdf.enabled')){
$this->app->bind('snappy.pdf', function($app)
{
$binary = $app['config']->get('snappy.pdf.binary', '/usr/local/bin/wkhtmltopdf');
$options = $app['config']->get('snappy.pdf.options', array());
$env = $app['config']->get('snappy.pdf.env', array());
$timeout = $app['config']->get('snappy.pdf.timeout', false);
$snappy = new IlluminateSnappyPdf($app['files'], $binary, $options, $env);
if (false !== $timeout) {
$snappy->setTimeout($timeout);
}
return $snappy;
});
$this->app->bind('snappy.pdf.wrapper', function($app)
{
return new PdfWrapper($app['snappy.pdf']);
});
$this->app->alias('snappy.pdf.wrapper', 'Barryvdh\Snappy\PdfWrapper');
}
if($this->app['config']->get('snappy.image.enabled')){
$this->app->bind('snappy.image', function($app)
{
$binary = $app['config']->get('snappy.image.binary', '/usr/local/bin/wkhtmltoimage');
$options = $app['config']->get('snappy.image.options', array());
$env = $app['config']->get('snappy.image.env', array());
$timeout = $app['config']->get('snappy.image.timeout', false);
$image = new IlluminateSnappyImage($app['files'], $binary, $options, $env);
if (false !== $timeout) {
$image->setTimeout($timeout);
}
return $image;
});
$this->app->bind('snappy.image.wrapper', function($app)
{
return new ImageWrapper($app['snappy.image']);
});
$this->app->alias('snappy.image.wrapper', 'Barryvdh\Snappy\ImageWrapper');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return array('snappy.pdf', 'snappy.pdf.wrapper', 'snappy.image', 'snappy.image.wrapper');
}
}
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
}
}
return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../doctrine/dbal/bin/doctrine-dbal)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal');
}
}
return include __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
}
}
return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
}
}
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../squizlabs/php_codesniffer/bin/phpcbf)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf');
}
}
return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../squizlabs/php_codesniffer/bin/phpcs)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs');
}
}
return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpstan/phpstan/phpstan)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan');
}
}
return include __DIR__ . '/..'.'/phpstan/phpstan/phpstan';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpstan/phpstan/phpstan.phar)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar');
}
}
return include __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar';
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = 'phpvfscomposer://'.$this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
}
}
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
}
}
return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
}
}
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath $selfArg 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../h4cc/wkhtmltoimage-amd64/bin' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${dir}/wkhtmltoimage-amd64" "$@"
return
fi
fi
"${dir}/wkhtmltoimage-amd64" "$@"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath $selfArg 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../h4cc/wkhtmltopdf-amd64/bin' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${dir}/wkhtmltopdf-amd64" "$@"
return
fi
fi
"${dir}/wkhtmltopdf-amd64" "$@"
+445
View File
@@ -0,0 +1,445 @@
# Changelog
All notable changes to this project will be documented in this file.
## [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.
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-present Benjamin Morel
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+34
View File
@@ -0,0 +1,34 @@
{
"name": "brick/math",
"description": "Arbitrary-precision arithmetic library",
"type": "library",
"keywords": [
"Brick",
"Math",
"Arbitrary-precision",
"Arithmetic",
"BigInteger",
"BigDecimal",
"BigRational",
"Bignum"
],
"license": "MIT",
"require": {
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"php-coveralls/php-coveralls": "^2.2",
"vimeo/psalm": "5.0.0"
},
"autoload": {
"psr-4": {
"Brick\\Math\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Brick\\Math\\Tests\\": "tests/"
}
}
}
@@ -0,0 +1,786 @@
<?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 string $value;
/**
* The scale (number of digits after the decimal point) of this decimal number.
*
* This must be zero or more.
*/
private 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;
}
/**
* Creates a BigDecimal of the given value.
*
* @throws MathException If the value cannot be converted to a BigDecimal.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigDecimal
{
return parent::of($value)->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 int $roundingMode An optional rounding mode.
*
* @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, int $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 this 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 this 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.
*
* @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, int $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'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->value . ':' . $this->scale;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->value)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$value, $scale] = \explode(':', $value);
$this->value = $value;
$this->scale = (int) $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,512 @@
<?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 \Serializable, \JsonSerializable
{
/**
* The regular expression used to parse integer, decimal and rational numbers.
*/
private const PARSE_REGEXP =
'/^' .
'(?<sign>[\-\+])?' .
'(?:' .
'(?:' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
')|(?:' .
'(?<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
*/
public static function of(BigNumber|int|float|string $value) : BigNumber
{
if ($value instanceof BigNumber) {
return $value;
}
if (\is_int($value)) {
return new BigInteger((string) $value);
}
$value = \is_float($value) ? self::floatToString($value) : $value;
$throw = static function() use ($value) : void {
throw new NumberFormatException(\sprintf(
'The given value "%s" does not represent a valid number.',
$value
));
};
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
$throw();
}
$getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null;
$sign = $getMatch('sign');
$numerator = $getMatch('numerator');
$denominator = $getMatch('denominator');
if ($numerator !== null) {
assert($denominator !== null);
if ($sign !== null) {
$numerator = $sign . $numerator;
}
$numerator = self::cleanUp($numerator);
$denominator = self::cleanUp($denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
return new BigRational(
new BigInteger($numerator),
new BigInteger($denominator),
false
);
}
$point = $getMatch('point');
$integral = $getMatch('integral');
$fractional = $getMatch('fractional');
$exponent = $getMatch('exponent');
if ($integral === null && $fractional === null) {
$throw();
}
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);
}
/**
* Safely converts float to string, avoiding locale-dependent issues.
*
* @see https://github.com/brick/math/pull/20
*
* @psalm-pure
* @psalm-suppress ImpureFunctionCall
*/
private static function floatToString(float $float) : string
{
$currentLocale = \setlocale(LC_NUMERIC, '0');
\setlocale(LC_NUMERIC, 'C');
$result = (string) $float;
\setlocale(LC_NUMERIC, $currentLocale);
return $result;
}
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigInteger(string $value) : BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
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
*/
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-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
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-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
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
*/
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 + sign from the given number.
*
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
*
* @psalm-pure
*/
private static function cleanUp(string $number) : string
{
$firstChar = $number[0];
if ($firstChar === '+' || $firstChar === '-') {
$number = \substr($number, 1);
}
$number = \ltrim($number, '0');
if ($number === '') {
return '0';
}
if ($firstChar === '-') {
return '-' . $number;
}
return $number;
}
/**
* Checks if this number is equal to the given one.
*/
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.
*/
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.
*/
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.
*/
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.
*/
public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*/
public function isZero() : bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*/
public function isNegative() : bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*/
public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*/
public function isPositive() : bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*/
public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
/**
* Returns the sign of this number.
*
* @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.
*
* @return int [-1,0,1] If `$this` is lower than, equal to, or 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 int $roundingMode A `RoundingMode` constant.
*
* @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, int $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;
public function jsonSerialize() : string
{
return $this->__toString();
}
}
@@ -0,0 +1,445 @@
<?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 BigInteger $numerator;
/**
* The denominator. Always strictly positive.
*/
private 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;
}
/**
* Creates a BigRational of the given value.
*
* @throws MathException If the value cannot be converted to a BigRational.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigRational
{
return parent::of($value)->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[]
*/
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, int $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'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->numerator)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$numerator, $denominator] = \explode('/', $value);
$this->numerator = BigInteger::of($numerator);
$this->denominator = BigInteger::of($denominator);
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a division by zero occurs.
*/
class DivisionByZeroException extends MathException
{
/**
* @psalm-pure
*/
public static function divisionByZero() : DivisionByZeroException
{
return new self('Division by zero.');
}
/**
* @psalm-pure
*/
public static function modulusMustNotBeZero() : DivisionByZeroException
{
return new self('The modulus must not be zero.');
}
/**
* @psalm-pure
*/
public static function denominatorMustNotBeZero() : DivisionByZeroException
{
return new self('The denominator of a rational number cannot be zero.');
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
/**
* Exception thrown when an integer overflow occurs.
*/
class IntegerOverflowException extends MathException
{
/**
* @psalm-pure
*/
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
{
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Base class for all math exceptions.
*/
class MathException extends \Exception
{
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/
class NegativeNumberException extends MathException
{
}
@@ -0,0 +1,33 @@
<?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
{
/**
* @param string $char The failing character.
*
* @psalm-pure
*/
public static function charNotInAlphabet(string $char) : self
{
$ord = \ord($char);
if ($ord < 32 || $ord > 126) {
$char = \strtoupper(\dechex($ord));
if ($ord < 10) {
$char = '0' . $char;
}
} else {
$char = '"' . $char . '"';
}
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a number cannot be represented at the requested scale without rounding.
*/
class RoundingNecessaryException extends MathException
{
/**
* @psalm-pure
*/
public static function roundingNecessary() : RoundingNecessaryException
{
return new self('Rounding is necessary to represent the result of the operation at this scale.');
}
}
@@ -0,0 +1,676 @@
<?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 = 1000000;
/**
* 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.
*
* @return int [-1, 0, 1] If the first number is less than, equal to, or 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 int $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, int $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);
}
switch ($operator) {
case 'and':
$value = $aBin & $bBin;
$negative = ($aNeg and $bNeg);
break;
case 'or':
$value = $aBin | $bBin;
$negative = ($aNeg or $bNeg);
break;
case 'xor':
$value = $aBin ^ $bBin;
$negative = ($aNeg xor $bNeg);
break;
// @codeCoverageIgnoreStart
default:
throw new \InvalidArgumentException('Invalid bitwise operator.');
// @codeCoverageIgnoreEnd
}
if ($negative) {
$value = $this->twosComplement($value);
}
$result = $this->toDecimal($value);
return $negative ? $this->neg($result) : $result;
}
/**
* @param string $number A positive, binary number.
*/
private function twosComplement(string $number) : string
{
$xor = \str_repeat("\xff", \strlen($number));
$number ^= $xor;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$byte = \ord($number[$i]);
if (++$byte !== 256) {
$number[$i] = \chr($byte);
break;
}
$number[$i] = "\x00";
if ($i === 0) {
$number = "\x01" . $number;
}
}
return $number;
}
/**
* Converts a decimal number to a binary string.
*
* @param string $number The number to convert, positive or zero, only digits.
*/
private function toBinary(string $number) : string
{
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, '256');
$result .= \chr((int) $remainder);
}
return \strrev($result);
}
/**
* Returns the positive decimal representation of a binary number.
*
* @param string $bytes The bytes representing the number.
*/
private function toDecimal(string $bytes) : string
{
$result = '0';
$power = '1';
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
$index = \ord($bytes[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
);
}
if ($i !== 0) {
$power = $this->mul($power, '256');
}
}
return $result;
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* 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);
}
/**
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
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);
assert($r !== null);
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);
}
/**
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function sqrt(string $n) : string
{
return \bcsqrt($n, 0);
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation built around the GMP library.
*
* @internal
*
* @psalm-immutable
*/
class GmpCalculator extends Calculator
{
public function add(string $a, string $b) : string
{
return \gmp_strval(\gmp_add($a, $b));
}
public function sub(string $a, string $b) : string
{
return \gmp_strval(\gmp_sub($a, $b));
}
public function mul(string $a, string $b) : string
{
return \gmp_strval(\gmp_mul($a, $b));
}
public function divQ(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_q($a, $b));
}
public function divR(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_r($a, $b));
}
public function divQR(string $a, string $b) : array
{
[$q, $r] = \gmp_div_qr($a, $b);
return [
\gmp_strval($q),
\gmp_strval($r)
];
}
public function pow(string $a, int $e) : string
{
return \gmp_strval(\gmp_pow($a, $e));
}
public function modInverse(string $x, string $m) : ?string
{
$result = \gmp_invert($x, $m);
if ($result === false) {
return null;
}
return \gmp_strval($result);
}
public function modPow(string $base, string $exp, string $mod) : string
{
return \gmp_strval(\gmp_powm($base, $exp, $mod));
}
public function gcd(string $a, string $b) : string
{
return \gmp_strval(\gmp_gcd($a, $b));
}
public function fromBase(string $number, int $base) : string
{
return \gmp_strval(\gmp_init($number, $base));
}
public function toBase(string $number, int $base) : string
{
return \gmp_strval($number, $base);
}
public function and(string $a, string $b) : string
{
return \gmp_strval(\gmp_and($a, $b));
}
public function or(string $a, string $b) : string
{
return \gmp_strval(\gmp_or($a, $b));
}
public function xor(string $a, string $b) : string
{
return \gmp_strval(\gmp_xor($a, $b));
}
public function sqrt(string $n) : string
{
return \gmp_strval(\gmp_sqrt($n));
}
}
@@ -0,0 +1,581 @@
<?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 int $maxDigits;
/**
* @codeCoverageIgnore
*/
public function __construct()
{
switch (PHP_INT_SIZE) {
case 4:
$this->maxDigits = 9;
break;
case 8:
$this->maxDigits = 18;
break;
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.
$r = $na % $nb;
$q = ($na - $r) / $nb;
assert(is_int($q));
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.
*
* @return int [-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,107 @@
<?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.
*/
final class RoundingMode
{
/**
* Private constructor. This class is not instantiable.
*
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* 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.
*/
public const UNNECESSARY = 0;
/**
* 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.
*/
public const UP = 1;
/**
* 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.
*/
public const DOWN = 2;
/**
* 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.
*/
public const CEILING = 3;
/**
* 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.
*/
public const FLOOR = 4;
/**
* 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.
*/
public const HALF_UP = 5;
/**
* 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.
*/
public const HALF_DOWN = 6;
/**
* 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.
*/
public const HALF_CEILING = 7;
/**
* 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.
*/
public const HALF_FLOOR = 8;
/**
* 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.
*/
public const HALF_EVEN = 9;
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Carbon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,14 @@
# carbonphp/carbon-doctrine-types
Types to use Carbon in Doctrine
## Documentation
[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/)
This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
from `nestbot/carbon` package.
Externalization allows to better deal with different versions of dbal. With
version 4.0 of dbal, it no longer sustainable to be compatible with all version
using a single code.
@@ -0,0 +1,36 @@
{
"name": "carbonphp/carbon-doctrine-types",
"description": "Types to use Carbon in Doctrine",
"type": "library",
"keywords": [
"date",
"time",
"DateTime",
"Carbon",
"Doctrine"
],
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/dbal": "^3.7.0",
"nesbot/carbon": "^2.71.0 || ^3.0.0",
"phpunit/phpunit": "^10.3"
},
"conflict": {
"doctrine/dbal": "<3.7.0 || >=4.0.0"
},
"license": "MIT",
"autoload": {
"psr-4": {
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
}
},
"authors": [
{
"name": "KyleKatarn",
"email": "kylekatarnls@gmail.com"
}
],
"minimum-stability": "dev"
}
@@ -0,0 +1,14 @@
<?php
namespace Carbon\Doctrine;
use Doctrine\DBAL\Platforms\AbstractPlatform;
interface CarbonDoctrineType
{
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
public function convertToPHPValue($value, AbstractPlatform $platform);
public function convertToDatabaseValue($value, AbstractPlatform $platform);
}
@@ -0,0 +1,7 @@
<?php
namespace Carbon\Doctrine;
class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
{
}
@@ -0,0 +1,7 @@
<?php
namespace Carbon\Doctrine;
class CarbonType extends DateTimeType implements CarbonDoctrineType
{
}
@@ -0,0 +1,141 @@
<?php
namespace Carbon\Doctrine;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Exception;
/**
* @template T of CarbonInterface
*/
trait CarbonTypeConverter
{
/**
* This property differentiates types installed by carbonphp/carbon-doctrine-types
* from the ones embedded previously in nesbot/carbon source directly.
*
* @readonly
*/
public bool $external = true;
/**
* @return class-string<T>
*/
protected function getCarbonClassName(): string
{
return Carbon::class;
}
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
$precision = min(
$fieldDeclaration['precision'] ?? DateTimeDefaultPrecision::get(),
$this->getMaximumPrecision($platform),
);
$type = parent::getSQLDeclaration($fieldDeclaration, $platform);
if (!$precision) {
return $type;
}
if (str_contains($type, '(')) {
return preg_replace('/\(\d+\)/', "($precision)", $type);
}
[$before, $after] = explode(' ', "$type ");
return trim("$before($precision) $after");
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return T|null
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$class = $this->getCarbonClassName();
if ($value === null || is_a($value, $class)) {
return $value;
}
if ($value instanceof DateTimeInterface) {
return $class::instance($value);
}
$date = null;
$error = null;
try {
$date = $class::parse($value);
} catch (Exception $exception) {
$error = $exception;
}
if (!$date) {
throw ConversionException::conversionFailedFormat(
$value,
$this->getTypeName(),
'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
$error
);
}
return $date;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return $value;
}
if ($value instanceof DateTimeInterface) {
return $value->format('Y-m-d H:i:s.u');
}
throw ConversionException::conversionFailedInvalidType(
$value,
$this->getTypeName(),
['null', 'DateTime', 'Carbon']
);
}
private function getTypeName(): string
{
$chunks = explode('\\', static::class);
$type = preg_replace('/Type$/', '', end($chunks));
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $type));
}
private function getMaximumPrecision(AbstractPlatform $platform): int
{
if ($platform instanceof DB2Platform) {
return 12;
}
if ($platform instanceof OraclePlatform) {
return 9;
}
if ($platform instanceof SQLServerPlatform || $platform instanceof SqlitePlatform) {
return 3;
}
return 6;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Carbon\Doctrine;
class DateTimeDefaultPrecision
{
private static $precision = 6;
/**
* Change the default Doctrine datetime and datetime_immutable precision.
*
* @param int $precision
*/
public static function set(int $precision): void
{
self::$precision = $precision;
}
/**
* Get the default Doctrine datetime and datetime_immutable precision.
*
* @return int
*/
public static function get(): int
{
return self::$precision;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Carbon\Doctrine;
use Carbon\CarbonImmutable;
use Doctrine\DBAL\Types\VarDateTimeImmutableType;
class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
{
/** @use CarbonTypeConverter<CarbonImmutable> */
use CarbonTypeConverter;
/**
* @return class-string<CarbonImmutable>
*/
protected function getCarbonClassName(): string
{
return CarbonImmutable::class;
}
}
@@ -0,0 +1,12 @@
<?php
namespace Carbon\Doctrine;
use Carbon\Carbon;
use Doctrine\DBAL\Types\VarDateTimeType;
class DateTimeType extends VarDateTimeType implements CarbonDoctrineType
{
/** @use CarbonTypeConverter<Carbon> */
use CarbonTypeConverter;
}
+579
View File
@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.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',
'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',
'9b38cf48e83f5d8f60375221cd213eee' => $vendorDir . '/phpstan/phpstan/bootstrap.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,11 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Milon\\Barcode' => array($vendorDir . '/milon/barcode/src'),
'Laracasts\\Flash' => array($vendorDir . '/laracasts/flash/src'),
);

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