This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="phpstan" version="2.1.17" installed="2.1.17" location="./tools/phpstan" copy="false"/>
</phive>
+10
View File
@@ -0,0 +1,10 @@
# Basic docker based environment
# Necessary to trick dokku into building the documentation
# using dockerfile instead of herokuish
FROM php:8.1
WORKDIR /code
VOLUME ["/code"]
CMD [ '/bin/bash' ]
+20
View File
@@ -0,0 +1,20 @@
Copyright (C) Brian Nesbitt
Copyright (C) Cake Software Foundation, Inc. (https://cakefoundation.org)
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.
+86
View File
@@ -0,0 +1,86 @@
# CakePHP Chronos
![Build Status](https://github.com/cakephp/chronos/actions/workflows/ci.yml/badge.svg?branch=master)
[![Latest Stable Version](https://img.shields.io/github/v/release/cakephp/chronos?sort=semver&style=flat-square)](https://packagist.org/packages/cakephp/chronos)
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/chronos?style=flat-square)](https://packagist.org/packages/cakephp/chronos/stats)
[![Code Coverage](https://img.shields.io/coveralls/cakephp/chronos/master.svg?style=flat-square)](https://coveralls.io/r/cakephp/chronos?branch=master)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
Chronos focuses on providing immutable date/datetime objects.
Immutable objects help ensure that datetime objects aren't accidentally
modified, keeping data more predictable.
# Installation
Installing with composer:
```
$ composer require cakephp/chronos
```
For details on the (minimum/maximum) PHP version see [version map](https://github.com/cakephp/chronos/wiki#version-map).
# Usage
```php
<?php
require 'vendor/autoload.php';
use Cake\Chronos\Chronos;
printf("Now: %s", Chronos::now());
```
# Differences with nesbot/carbon
Chronos was originally compatible with Carbon but has diverged and no longer
extends the PHP DateTime and DateTimeImmutable classes.
# Immutable Object Changes
Immutable objects have a number of advantages:
1. Using immutable objects is always free of side-effects.
2. Dates and times don't accidentally change underneath other parts of your code.
With those benefits in mind, there are a few things you need to keep in mind
when modifying immutable objects:
```php
// This will lose modifications
$date = new Chronos('2015-10-21 16:29:00');
$date->modify('+2 hours');
// This will keep modifications
$date = new Chronos('2015-10-21 16:29:00');
$date = $date->modify('+2 hours');
```
# Calendar Dates
PHP only offers datetime objects as part of the native extensions. Chronos adds
a number of conveniences to the traditional DateTime object and introduces
a `ChronosDate` object. `ChronosDate` instances their time frozen to `00:00:00` and the timezone
set to the server default timezone. This makes them ideal when working with
calendar dates as the time components will always match.
```php
use Cake\Chronos\ChronosDate;
$today = new ChronosDate();
echo $today;
// Outputs '2015-10-21'
echo $today->modify('+3 hours');
// Outputs '2015-10-21'
```
Like instances of `Chronos`, `ChronosDate` objects are also *immutable*.
# Documentation
A more descriptive documentation can be found at [book.cakephp.org/chronos/3/en/](https://book.cakephp.org/chronos/3/en/).
# API Documentation
API documentation can be found on [api.cakephp.org/chronos](https://api.cakephp.org/chronos).
+67
View File
@@ -0,0 +1,67 @@
{
"name": "cakephp/chronos",
"description": "A simple API extension for DateTime.",
"license": "MIT",
"type": "library",
"keywords": [
"date",
"time",
"DateTime"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"homepage": "https://cakephp.org",
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.1.3"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Cake\\Chronos\\Test\\": "tests/"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
"check": [
"@test",
"@cs-check",
"@stan"
],
"cs-check": "phpcs --colors --parallel=16 -p",
"cs-fix": "phpcbf --colors --parallel=16 -p",
"phpstan": "tools/phpstan analyse",
"stan": "@phpstan",
"stan-baseline": "tools/phpstan --generate-baseline",
"stan-setup": "phive install",
"test": "phpunit"
}
}
+26
View File
@@ -0,0 +1,26 @@
# Generate the HTML output.
FROM ghcr.io/cakephp/docs-builder as builder
RUN pip install git+https://github.com/sphinx-contrib/video.git@master
COPY docs /data/docs
ENV LANGS="en fr ja pt"
# build docs with sphinx
RUN cd /data/docs-builder && \
make website LANGS="$LANGS" SOURCE=/data/docs DEST=/data/website
# Build a small nginx container with just the static site in it.
FROM ghcr.io/cakephp/docs-builder:runtime as runtime
ENV LANGS="en fr ja pt"
ENV SEARCH_SOURCE="/usr/share/nginx/html"
ENV SEARCH_URL_PREFIX="/chronos/3"
COPY --from=builder /data/docs /data/docs
COPY --from=builder /data/website /data/website
COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf
# Move docs into place.
RUN cp -R /data/website/html/* /usr/share/nginx/html \
&& rm -rf /data/website
+50
View File
@@ -0,0 +1,50 @@
# Global configuration information used across all the
# translations of documentation.
#
# Import the base theme configuration
from cakephpsphinx.config.all import *
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
release = '3.x'
# The search index version.
search_version = 'chronos-3'
# The marketing display name for the book.
version_name = ''
# Project name shown in the black header bar
project = 'Chronos'
# Other versions that display in the version picker menu.
version_list = [
{'name': '1.x', 'number': '/chronos/1', 'title': '1.x'},
{'name': '2.x', 'number': '/chronos/2', 'title': '2.x'},
{'name': '3.x', 'number': '/chronos/3', 'title': '3.x', 'current': True},
]
# Languages available.
languages = ['en', 'fr', 'ja', 'pt']
# The GitHub branch name for this version of the docs
# for edit links to point at.
branch = '3.x'
# Current version being built
version = '3.x'
# Language in use for this directory.
language = 'en'
show_root_link = True
repository = 'cakephp/chronos'
source_path = 'docs/'
hide_page_contents = ('search', '404', 'contents')
@@ -0,0 +1,21 @@
3.x Migration Guide
###################
Chronos 3.x contains breaking changes that could impact your application. This
guide provides an overview of the breaking changes made in 3.x
Minimum of PHP 8.1
==================
Chronos 3.x requires at least PHP 8.1. This allows chronos to provide more
comprehensive typehinting and better performance by leveraging features found in
newer PHP versions.
MutableDateTime and MutableDate removed
=======================================
The ``MutableDateTime`` and ``MutableDate`` classes have been removed. Long term
PHP will be deprecating and removing mutable datetime classes in favour of
immutable ones. Chronos has long favoured immutable objects and removing the
mutable variants helps simplify the internals of Chronos and encourages safer
development practices.
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'en'
+7
View File
@@ -0,0 +1,7 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Chronos
/index
API <https://api.cakephp.org/chronos>
+308
View File
@@ -0,0 +1,308 @@
Chronos
#######
Chronos provides a zero-dependency ``DateTimeImmutable`` extension, Date-only and Time-only classes:
* ``Cake\Chronos\Chronos`` extends ``DateTimeImmutable`` and provides many helpers.
* ``Cake\Chronos\ChronosDate`` represents calendar dates unaffected by time or time zones.
* ``Cake\Chronos\ChronosTime`` represents clock times independent of date or time zones.
* Only safe, immutable objects.
* A pluggable translation system. Only English translations are included in the
library. However, ``cakephp/i18n`` can be used for full language support.
The ``Chronos`` class extends ``DateTimeImmutable`` and implements ``DateTimeInterface``
which allows users to use type declarations that support either.
``ChronosDate`` and ``ChronosTime`` do not extend ``DateTimeImmutable`` and do not
share an interface. However, they can be converted to a ``DateTimeImmutable`` instance
using ``toDateTimeImmutable()``.
Installation
------------
To install Chronos, you should use ``composer``. From your
application's ROOT directory (where composer.json file is located) run the
following::
php composer.phar require "cakephp/chronos:^3.0"
Creating Instances
------------------
There are many ways to get an instance of Chronos or Date. There are a number of
factory methods that work with different argument sets::
use Cake\Chronos\Chronos;
$now = Chronos::now();
$today = Chronos::today();
$yesterday = Chronos::yesterday();
$tomorrow = Chronos::tomorrow();
// Parse relative expressions
$date = Chronos::parse('+2 days, +3 hours');
// Date and time integer values.
$date = Chronos::create(2015, 12, 25, 4, 32, 58);
// Date or time integer values.
$date = Chronos::createFromDate(2015, 12, 25);
$date = Chronos::createFromTime(11, 45, 10);
// Parse formatted values.
$date = Chronos::createFromFormat('m/d/Y', '06/15/2015');
Working with Immutable Objects
------------------------------
Chronos provides only *immutable* objects.
If you've used PHP ``DateTimeImmutable`` and ``DateTime`` classes, then you understand
the difference between *mutable* and *immutable* objects.
Immutable objects create copies of an object each time a change is made. Because modifier methods
around datetimes are not always easy to identify, data can be modified accidentally
or without the developer knowing. Immutable objects prevent accidental changes
to data, and make code free of order-based dependency issues. Immutability does
mean that you will need to remember to replace variables when using modifiers::
// This code doesn't work with immutable objects
$chronos->addDay(1);
doSomething($chronos);
return $chronos;
// This works like you'd expect
$chronos = $chronos->addDay(1);
$chronos = doSomething($chronos);
return $chronos;
By capturing the return value of each modification your code will work as
expected.
Date Objects
------------
PHP provides only date-time classes that combines both dates and time parts.
Representing calendar dates can be a bit awkward with ``DateTimeImmutable`` as it includes
time and timezones, which aren't part of a 'date'. Chronos provides
``ChronosDate`` that allows you to represent dates. The time these objects
these objects is always fixed to ``00:00:00`` and not affeced by the server time zone
or modify helpers::
use Cake\Chronos\ChronosDate;
$today = ChronosDate::today();
// Changes to the time/timezone are ignored.
$today->modify('+1 hours');
// Outputs '2015-12-20'
echo $today;
Although ``ChronosDate`` uses a fixed time zone internally, you can specify which
time zone to use for current time such as ``now()`` or ``today()``::
use Cake\Chronos\ChronosDate:
// Takes the current date from Asia/Tokyo time zone
$today = ChronosDate::today('Asia/Tokyo');
Modifier Methods
----------------
Chronos objects provide modifier methods that let you modify the value in
a granular way::
// Set components of the datetime value.
$halloween = Chronos::create()
->year(2015)
->month(10)
->day(31)
->hour(20)
->minute(30);
You can also modify parts of the datetime relatively::
$future = Chronos::create()
->addYears(1)
->subMonths(2)
->addDays(15)
->addHours(20)
->subMinutes(2);
It is also possible to make big jumps to defined points in time::
$time = Chronos::create();
$time->startOfDay();
$time->endOfDay();
$time->startOfMonth();
$time->endOfMonth();
$time->startOfYear();
$time->endOfYear();
$time->startOfWeek();
$time->endOfWeek();
Or jump to specific days of the week::
$time->next(Chronos::TUESDAY);
$time->previous(Chronos::MONDAY);
When modifying dates/times across :abbr:`DST (Daylight Savings Time)` transitions
your operations may gain/lose an additional hours resulting in hour values that
don't add up. You can avoid these issues by first changing your timezone to
``UTC``, modifying the time::
// Additional hour gained.
$time = new Chronos('2014-03-30 00:00:00', 'Europe/London');
debug($time->modify('+24 hours')); // 2014-03-31 01:00:00
// First switch to UTC, and modify
$time = $time->setTimezone('UTC')
->modify('+24 hours');
Once you are done modifying the time you can add the original timezone to get
the localized time.
Comparison Methods
------------------
Once you have 2 instances of Chronos date/time objects you can compare them in
a variety of ways::
// Full suite of comparators exist
// equals, notEquals, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals
$first->equals($second);
$first->greaterThanOrEquals($second);
// See if the current object is between two others.
$now->between($start, $end);
// Find which argument is closest or farthest.
$now->closest($june, $november);
$now->farthest($june, $november);
You can also inquire about where a given value falls on the calendar::
$now->isToday();
$now->isYesterday();
$now->isFuture();
$now->isPast();
// Check the day of the week
$now->isWeekend();
// All other weekday methods exist too.
$now->isMonday();
You can also find out if a value was within a relative time period::
$time->wasWithinLast('3 days');
$time->isWithinNext('3 hours');
Generating Differences
----------------------
In addition to comparing datetimes, calculating differences or deltas between
two values is a common task::
// Get a DateInterval representing the difference
$first->diff($second);
// Get difference as a count of specific units.
$first->diffInHours($second);
$first->diffInDays($second);
$first->diffInWeeks($second);
$first->diffInYears($second);
You can generate human readable differences suitable for use in a feed or
timeline::
// Difference from now.
echo $date->diffForHumans();
// Difference from another point in time.
echo $date->diffForHumans($other); // 1 hour ago;
Formatting Strings
------------------
Chronos provides a number of methods for displaying our outputting datetime
objects::
// Uses the format controlled by setToStringFormat()
echo $date;
// Different standard formats
echo $time->toAtomString(); // 1975-12-25T14:15:16-05:00
echo $time->toCookieString(); // Thursday, 25-Dec-1975 14:15:16 EST
echo $time->toIso8601String(); // 1975-12-25T14:15:16-05:00
echo $time->toRfc822String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc850String(); // Thursday, 25-Dec-75 14:15:16 EST
echo $time->toRfc1036String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc1123String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc2822String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc3339String(); // 1975-12-25T14:15:16-05:00
echo $time->toRssString(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toW3cString(); // 1975-12-25T14:15:16-05:00
// Get the quarter/week
echo $time->toQuarter(); // 4
echo $time->toWeek(); // 52
// Generic formatting
echo $time->toTimeString(); // 14:15:16
echo $time->toDateString(); // 1975-12-25
echo $time->toDateTimeString(); // 1975-12-25 14:15:16
echo $time->toFormattedDateString(); // Dec 25, 1975
echo $time->toDayDateTimeString(); // Thu, Dec 25, 1975 2:15 PM
Extracting Date Components
--------------------------
Getting parts of a date object can be done by directly accessing properties::
$time = new Chronos('2015-12-31 23:59:58.123');
$time->year; // 2015
$time->month; // 12
$time->day; // 31
$time->hour // 23
$time->minute // 59
$time->second // 58
$time->micro // 123
Other properties that can be accessed are:
- timezone
- timezoneName
- dayOfWeek
- dayOfMonth
- dayOfYear
- daysInMonth
- timestamp
- quarter
- half
Testing Aids
------------
When writing unit tests, it is helpful to fixate the current time. Chronos lets
you fix the current time for each class. As part of your test suite's bootstrap
process you can include the following::
Chronos::setTestNow(Chronos::now());
ChronosDate::setTestNow(ChronosDate::parse(Chronos::now()));
This will fix the current time of all objects to be the point at which the test
suite started.
For example, if you fixate the ``Chronos`` to some moment in the past, any new
instance of ``Chronos`` created with ``now`` or a relative time string, will be
returned relative to the fixated time::
Chronos::setTestNow(new Chronos('1975-12-25 00:00:00'));
$time = new Chronos(); // 1975-12-25 00:00:00
$time = new Chronos('1 hour ago'); // 1975-12-24 23:00:00
To reset the fixation, simply call ``setTestNow()`` again with no parameter or
with ``null`` as a parameter.
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'fr'
+7
View File
@@ -0,0 +1,7 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Chronos
/index
API <https://api.cakephp.org/chronos>
+329
View File
@@ -0,0 +1,329 @@
Chronos
#######
Chronos fournit une collection d'extensions sans aucune dépendance pour l'objet
``DateTime``. En plus de méthodes pratiques, Chronos fournit:
* Des objets ``Date`` pour représenter les dates du calendrier.
* Des objets immutables pour les dates et les datetimes.
* Un système de traduction intégrable. Seules les traductions anglaises sont
incluses dans la librairie. Cependant, ``cakephp/i18n`` peut être utilisé
pour un support complet d'autres langues.
Installation
------------
Pour installer Chronos, vous devez utiliser ``composer``. À partir du répertoire
ROOT de votre application (celui où se trouve le fichier composer.json),
exécutez ce qui suit::
php composer.phar require "cakephp/chronos:^2.0"
Vue d'Ensemble
--------------
Chronos fournit un certain nombre d'extensions pour les objets DateTime fournis
par PHP. Chronos fournit 5 classes qui gèrent les variantes mutables et
immutables de date/time et les extensions de ``DateInterval``.
* ``Cake\Chronos\Chronos`` est un objet de *date et heure* immutable.
* ``Cake\Chronos\ChronosDate`` est un objet de *date* immutable.
* ``Cake\Chronos\MutableDateTime`` est un objet de *date et heure* mutable.
* ``Cake\Chronos\MutableDate`` est un objet de *date* mutable.
* ``Cake\Chronos\ChronosInterval`` est une extension pour l'objet
``DateInterval``.
Créer des Instances
-------------------
Il y a plusieurs façons d'obtenir une instance de Chronos ou de Date. Il y a
un certain nombre de méthodes factory qui fonctionnent avec différents ensembles
d'arguments::
use Cake\Chronos\Chronos;
$now = Chronos::now();
$today = Chronos::today();
$yesterday = Chronos::yesterday();
$tomorrow = Chronos::tomorrow();
// Parse les expressions relatives
$date = Chronos::parse('+2 days, +3 hours');
// Des entiers indiquant la date et l'heure.
$date = Chronos::create(2015, 12, 25, 4, 32, 58);
// Des entiers indiquant la date ou l'heure.
$date = Chronos::createFromDate(2015, 12, 25);
$date = Chronos::createFromTime(11, 45, 10);
// Parse les valeurs formatées.
$date = Chronos::createFromFormat('m/d/Y', '06/15/2015');
Travailler avec les Objets Immutables
-------------------------------------
Si vous avez utilisé les objets ``DateTime`` de PHP, vous êtes à l'aise avec
les objets *mutable*. Chronos offre des objets mutables, mais elle fournit
également des objets *immutables*. Les objets Immutables créent des copies des
objets à chaque fois qu'un objet est modifié. Puisque les méthodes de
modification autour des datetimes ne sont pas toujours transparentes, les
données peuvent être modifiées accidentellement ou sans que le développeur ne
le sache. Les objets immutables évitent les changements accidentels des
données et permettent de s'affranchir de tout problème lié à l'ordre d'appel
des fonctions ou des dépendances. L'immutabilité signifie que vous devez vous
souvenir de remplacer les variables quand vous utilisez les modificateurs::
// Ce code ne fonctionne pas avec les objets immutables
$time->addDay(1);
doSomething($time);
return $time;
// Ceci fonctionne comme vous le souhaitez
$time = $time->addDay(1);
$time = doSomething($time);
return $time;
En capturant la valeur de retour pour chaque modification, votre code
fonctionnera comme souhaité. Si vous avez déjà créé un objet immutable, et que
vous souhaitez un objet mutable, vous pouvez utiliser ``toMutable()``::
$inplace = $time->toMutable();
Objets Date
-----------
PHP fournit seulement un unique objet DateTime. Représenter les dates de
calendrier peut être un peu gênant avec cette classe puisqu'elle inclut les
timezones, et les composants de time qui n'appartiennent pas vraiment
au concept d'un 'jour'. Chronos fournit un objet ``Date`` qui vous permet
de représenter les dates. Les time et timezone pour ces objets sont toujours
fixés à ``00:00:00 UTC`` et toutes les méthodes de formatage/différence
fonctionnent au niveau du jour::
use Cake\Chronos\ChronosDate;
$today = ChronosDate::today();
// Les changements selon le time/timezone sont ignorés.
$today->modify('+1 hours');
// Affiche '2015-12-20'
echo $today;
Bien que ``Date`` utilise en interne un fuseau horaire fixe, vous pouvez
spécifier le fuseau à utiliser pour l'heure courante telle que ``now()`` ou
``today()``::
use Cake\Chronos\ChronosDate:
// Prend l'heure courante pour le fuseau horaire de Tokyo
$today = ChronosDate::today('Asia/Tokyo');
Méthodes de Modification
------------------------
Les objets Chronos fournissent des méthodes de modification qui vous laissent
modifier la valeur d'une façon assez précise::
// Définit les composants de la valeur du datetime.
$halloween = Chronos::create()
->year(2015)
->month(10)
->day(31)
->hour(20)
->minute(30);
Vous pouvez aussi modifier les parties de la date de façon relative::
$future = Chronos::create()
->addYear(1)
->subMonth(2)
->addDays(15)
->addHours(20)
->subMinutes(2);
Il est également possible de faire des sauts vers des points définis dans le
temps::
$time = Chronos::create();
$time->startOfDay();
$time->endOfDay();
$time->startOfMonth();
$time->endOfMonth();
$time->startOfYear();
$time->endOfYear();
$time->startOfWeek();
$time->endOfWeek();
Ou de sauter à un jour spécifique de la semaine::
$time->next(Chronos::TUESDAY);
$time->previous(Chronos::MONDAY);
Quand vous modifiez des dates/heures au-delà d'un passage à l'heure d'été ou à
l'heure d'hiver, vous opérations peuvent gagner/perdre une heure de plus, de
sorte que les heures seront incorrectes. Vous pouvez éviter ce problème en
définissant d'abord le timezone à ``UTC``, ce qui change l'heure::
// Une heure de plus de gagnée.
$time = new Chronos('2014-03-30 00:00:00', 'Europe/London');
debug($time->modify('+24 hours')); // 2014-03-31 01:00:00
// Passez d'abord à UTC, et modifiez ensuite
$time = $time->setTimezone('UTC')
->modify('+24 hours');
Une fois que vous avez modifié l'heure, vous pouvez repasser au timezone
d'origine pour obtenir l'heure locale.
Méthodes de Comparaison
-----------------------
Une fois que vous avez 2 instances d'objets date/time de Chronos, vous pouvez
les comparer de plusieurs façons::
// Il existe une suite complète de comparateurs
// equals, notEquals, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals
$first->equals($second);
$first->greaterThanOrEquals($second);
// Regarder si l'objet courant est entre deux autres.
$now->between($start, $end);
// Trouver l'argument le plus proche ou le plus éloigné.
$now->closest($june, $november);
$now->farthest($june, $november);
Vous pouvez aussi vous renseigner sur le moment où une valeur donnée tombe dans
le calendrier::
$now->isToday();
$now->isYesterday();
$now->isFuture();
$now->isPast();
// Vérifie le jour de la semaine
$now->isWeekend();
// Toutes les autres méthodes des jours de la semaine existent aussi.
$now->isMonday();
Vous pouvez aussi trouver si une valeur était dans une période de temps relative::
$time->wasWithinLast('3 days');
$time->isWithinNext('3 hours');
Générer des Différences
-----------------------
En plus de comparer les datetimes, calculer les différences ou les deltas entre
des valeurs est une tâche courante::
// Récupère un DateInterval représentant la différence
$first->diff($second);
// Récupère la différence en tant que nombre d'unités spécifiques.
$first->diffInHours($second);
$first->diffInDays($second);
$first->diffInWeeks($second);
$first->diffInYears($second);
Vous pouvez générer des différences lisibles qui peuvent vous servir pour
l'utilisation d'un feed ou d'une timeline::
// Différence à partir de maintenant.
echo $date->diffForHumans();
// Différence à partir d'un autre point du temps.
echo $date->diffForHumans($other); // 1 hour ago;
Formater les Chaînes
--------------------
Chronos fournit un certain nombre de méthodes pour afficher nos sorties d'objets
datetime::
// Utilise le format contrôlé par setToStringFormat()
echo $date;
// Différents formats standards
echo $time->toAtomString(); // 1975-12-25T14:15:16-05:00
echo $time->toCookieString(); // Thursday, 25-Dec-1975 14:15:16 EST
echo $time->toIso8601String(); // 1975-12-25T14:15:16-05:00
echo $time->toRfc822String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc850String(); // Thursday, 25-Dec-75 14:15:16 EST
echo $time->toRfc1036String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc1123String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc2822String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc3339String(); // 1975-12-25T14:15:16-05:00
echo $time->toRssString(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toW3cString(); // 1975-12-25T14:15:16-05:00
// Récupère le trimestre
echo $time->toQuarter(); // 4;
// Récupère la semaine
echo $time->toWeek(); // 52;
// Formatage générique
echo $time->toTimeString(); // 14:15:16
echo $time->toDateString(); // 1975-12-25
echo $time->toDateTimeString(); // 1975-12-25 14:15:16
echo $time->toFormattedDateString(); // Dec 25, 1975
echo $time->toDayDateTimeString(); // Thu, Dec 25, 1975 2:15 PM
Extraire des Fragments de Date
------------------------------
Il est possible de récupérer des parties d'un objet date en accédant directement
à ses propriétés::
$time = new Chronos('2015-12-31 23:59:58.123');
$time->year; // 2015
$time->month; // 12
$time->day; // 31
$time->hour // 23
$time->minute // 59
$time->second // 58
$time->micro // 123
Les autres propriétés accessibles sont:
- timezone
- timezoneName
- dayOfWeek
- dayOfMonth
- dayOfYear
- daysInMonth
- timestamp
- quarter
- half
Aides aux Tests
---------------
Quand vous écrivez des tests unitaires, il peut être utile de fixer le *time*
courant. Chronos vous permet de fixer le time courant pour chaque classe.
Pour l'intégrer dans votre processus de démarrage (bootstrap) de suite de tests,
vous pouvez inclure ce qui suit::
Chronos::setTestNow(Chronos::now());
MutableDateTime::setTestNow(MutableDateTime::now());
ChronosDate::setTestNow(ChronosDate::parse(Chronos::now()));
MutableDate::setTestNow(MutableDate::now());
Ceci va fixer le time courant de tous les objets selon le moment où la suite de
tests a démarré.
Par exemple, si vous fixez le ``Chronos`` à un moment du passé, chaque nouvelle
instance de ``Chronos`` créée avec ``now`` ou une chaine de temps relative, sera
retournée relativement à la date fixée::
Chronos::setTestNow(new Chronos('1975-12-25 00:00:00'));
$time = new Chronos(); // 1975-12-25 00:00:00
$time = new Chronos('1 hour ago'); // 1975-12-24 23:00:00
Pour réinitialiser la "fixation" du temps, appelez simplement ``setTestNow()``
sans paramètre ou avec ``null`` comme paramètre.
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'ja'
+7
View File
@@ -0,0 +1,7 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Chronos
/index
API <https://api.cakephp.org/chronos>
+301
View File
@@ -0,0 +1,301 @@
Chronos
#######
Chronos (クロノス) は、 ``DateTime`` オブジェクトへの拡張の依存関係の無いコレクションを提供します。
便利なメソッドに加えて、Chronos は以下を提供します。
* カレンダー日付のための ``Date`` オブジェクト
* イミュータブルな日付と日時オブジェクト
* プラグインのような翻訳システム。ライブラリーは英語のみの翻訳を含んでいます。
しかし、全ての言語サポートのために、 ``cakephp/i18n`` を使うことができます。
インストール
------------
Chronos をインストールするためには、 ``composer`` を利用することができます。
アプリケーションの ROOT ディレクトリー(composer.json ファイルのある場所)
で以下のように実行します。 ::
php composer.phar require cakephp/chronos "@stable"
概要
----
Chronos は PHP が提供する DateTime オブジェクトのいくつかの拡張を提供します。
Chronos は ``DateInterval`` の拡張機能および、ミュータブル(変更可能)と
イミュータブル(変更不可)な 日付/時刻 の派生系をカバーする5つのクラスを提供します。
* ``Cake\Chronos\Chronos`` はイミュータブルな *日付と時刻* オブジェクト。
* ``Cake\Chronos\ChronosDate`` はイミュータブルな *日付* オブジェクト。
* ``Cake\Chronos\MutableDateTime`` はミュータブルな *日付と時刻* オブジェクト。
* ``Cake\Chronos\MutableDate`` はミュータブルな *日付* オブジェクト。
* ``Cake\Chronos\ChronosInterval````DateInterval`` の拡張機能。
インスタンスの作成
------------------
Chronos または Date のインスタンスを取得するためには、多くの方法があります。
異なる引数セットで動作する多くのファクトリーメソッドがあります。 ::
use Cake\Chronos\Chronos;
$now = Chronos::now();
$today = Chronos::today();
$yesterday = Chronos::yesterday();
$tomorrow = Chronos::tomorrow();
// 相対式のパース
$date = Chronos::parse('+2 days, +3 hours');
// 日付と時間の整数値
$date = Chronos::create(2015, 12, 25, 4, 32, 58);
// 日付または時間の整数値
$date = Chronos::createFromDate(2015, 12, 25);
$date = Chronos::createFromTime(11, 45, 10);
// 整形した値にパース
$date = Chronos::createFromFormat('m/d/Y', '06/15/2015');
イミュータブルオブジェクトの動作
--------------------------------
もしあなたが、PHP の ``DateTime`` オブジェクトを使用したことがあるなら、
*ミュータブル* オブジェクトは簡単に使用できます。
Chronos はミュータブルオブジェクトを提供しますが、これは *イミュータブル* オブジェクトにもなります。
イミュータブルオブジェクトはオブジェクトが変更されるたびにオブジェクトのコピーを作ります。
なぜなら、日時周りの変更メソッドは必ずしも透明でないため、データが誤って、
または開発者が知らない内に変更してしまうからです。
イミュータブルオブジェクトはデータが誤って変更されることを防止し、
順序ベースの依存関係の問題の無いコードを作ります。
不変性は、変更時に忘れずに変数を置き換える必要があることを意味しています。 ::
// このコードはイミュータブルオブジェクトでは動作しません
$time->addDay(1);
doSomething($time);
return $time
// このコードは期待通りに動作します
$time = $time->addDay(1);
$time = doSomething($time);
return $time
各修正の戻り値をキャプチャーすることによって、コードは期待通りに動作します。
イミュータブルオブジェクトを持っていて、ミュータブルオブジェクトを作りたい場合、
``toMutable()`` が使用できます。 ::
$inplace = $time->toMutable();
日付オブジェクト
------------------
PHP は単純な DateTime オブジェクトだけを提供します。このクラスのカレンダー日付の表現で、
タイムゾーンおよび、本当に「日」の概念に属していないタイムコンポーネントを含むと、
少し厄介な可能性があります。
Chronos は日時表現のための ``Date`` オブジェクトを提供します。
これらのオブジェクトの時間とタイムゾーンは常に ``00:00:00 UTC`` に固定されており、
全ての書式/差分のメソッドは一日単位で動作します。 ::
use Cake\Chronos\ChronosDate;
$today = ChronosDate::today();
// 時間/タイムゾーンの変更は無視されます
$today->modify('+1 hours');
// 出力 '2015-12-20'
echo $today;
変更メソッド
------------
Chronos オブジェクトは細やかに値を変更できるメソッドを提供します。 ::
// 日時の値のコンポーネントを設定
$halloween = Chronos::create()
->year(2015)
->month(10)
->day(31)
->hour(20)
->minute(30);
また、日時の部分を相対的に変更することもできます。 ::
$future = Chronos::create()
->addYear(1)
->subMonth(2)
->addDays(15)
->addHours(20)
->subMinutes(2);
また、ある時間の中で、定義された時点に飛ぶことも可能です。 ::
$time = Chronos::create();
$time->startOfDay();
$time->endOfDay();
$time->startOfMonth();
$time->endOfMonth();
$time->startOfYear();
$time->endOfYear();
$time->startOfWeek();
$time->endOfWeek();
また、1週間中の特定の日にも飛べます。 ::
$time->next(Chronos::TUESDAY);
$time->previous(Chronos::MONDAY);
:abbr:`DST (夏時間)` の遷移の前後で日付/時間を変更すると、
あなたの操作で時間が増減するかもしれませんが、その結果、意図しない時間の値になります。
これらの問題を回避するには、最初にタイムゾーンを ``UTC`` に変更し、時間を変更します。 ::
// 余分な時間が追加されました
$time = new Chronos('2014-03-30 00:00:00', 'Europe/London');
debug($time->modify('+24 hours')); // 2014-03-31 01:00:00
// 最初に UTC に切り替え、そして更新
$time = $time->setTimezone('UTC')
->modify('+24 hours');
時間を変更すると、元のタイムゾーンを追加してローカライズされた時間を取得することができます。
比較メソッド
------------
Chronos の日付/時間オブジェクトの2つのインスタンスを様々な方法で比較することができます。 ::
// 比較のフルセットが存在します
// equals, notEquals, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals
$first->equals($second);
$first->greaterThanOrEquals($second);
// カレントオブジェクトが2つのオブジェクトの間にあるかどうかを確認します。
$now->between($start, $end);
// どちらの引数が最も近い (closest) か、または最も遠い (farthest) かを見つけます。
$now->closest($june, $november);
$now->farthest($june, $november);
また、与えられた値のカレンダーに当たる場所について問い合わせできます。 ::
$now->isToday();
$now->isYesterday();
$now->isFuture();
$now->isPast();
// 曜日をチェック
$now->isWeekend();
// 他の曜日のメソッドも全て存在します。
$now->isMonday();
また、値が相対的な期間内にあったかどうかを見つけることができます。 ::
$time->wasWithinLast('3 days');
$time->isWithinNext('3 hours');
差の生成
--------
日時比較に加えて、2つの値の差や変化の計算は一般的なタスクです。 ::
// 差をあらわす DateInterval を取得
$first->diff($second);
// 特定の単位での差を取得
$first->diffInHours($second);
$first->diffInDays($second);
$first->diffInWeeks($second);
$first->diffInYears($second);
フィードやタイムラインで使用するのに適した、人が読める形式の差を生成することができます。 ::
// 現在からの差
echo $date->diffForHumans();
// 別の時点からの差
echo $date->diffForHumans($other); // 1時間前;
フォーマットの設定
------------------
Chronos は、出力した日時オブジェクトを表示するための多くのメソッドを提供します。 ::
// setToStringFormat() が制御するフォーマットを使用します
echo $date;
// 別の標準フォーマット
echo $time->toAtomString(); // 1975-12-25T14:15:16-05:00
echo $time->toCookieString(); // Thursday, 25-Dec-1975 14:15:16 EST
echo $time->toIso8601String(); // 1975-12-25T14:15:16-05:00
echo $time->toRfc822String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc850String(); // Thursday, 25-Dec-75 14:15:16 EST
echo $time->toRfc1036String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc1123String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc2822String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc3339String(); // 1975-12-25T14:15:16-05:00
echo $time->toRssString(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toW3cString(); // 1975-12-25T14:15:16-05:00
// クォーター/週数を取得
echo $time->toQuarter(); // 4;
echo $time->toWeek(); // 52
// 一般的なフォーマット
echo $time->toTimeString(); // 14:15:16
echo $time->toDateString(); // 1975-12-25
echo $time->toDateTimeString(); // 1975-12-25 14:15:16
echo $time->toFormattedDateString(); // Dec 25, 1975
echo $time->toDayDateTimeString(); // Thu, Dec 25, 1975 2:15 PM
日付要素の抽出
--------------
日付オブジェクトのプロパティーに直接アクセスして要素を取得することができます。 ::
$time = new Chronos('2015-12-31 23:59:58');
$time->year; // 2015
$time->month; // 12
$time->day; // 31
$time->hour // 23
$time->minute // 59
$time->second // 58
以下のプロパティーにもアクセスできます。 :
- timezone
- timezoneName
- micro
- dayOfWeek
- dayOfMonth
- dayOfYear
- daysInMonth
- timestamp
- quarter
- half
テストの支援
------------
単体テストを書いている時、現在時刻を固定すると便利です。Chronos は、
各クラスの現在時刻を修正することができます。
テストスイートの bootstrap 処理に以下を含めることができます。 ::
Chronos::setTestNow(Chronos::now());
MutableDateTime::setTestNow(MutableDateTime::now());
ChronosDate::setTestNow(ChronosDate::parse(Chronos::now()));
MutableDate::setTestNow(MutableDate::now());
これでテストスイートが開始された時点で全てのオブジェクトの現在時刻を修正します。
例えば、 ``Chronos`` を過去のある瞬間に固定した場合、新たな ``Chronos``
のインスタンスが生成する ``now`` または相対時刻の文字列は、
固定された時刻の相対を返却します。 ::
Chronos::setTestNow(new Chronos('1975-12-25 00:00:00'));
$time = new Chronos(); // 1975-12-25 00:00:00
$time = new Chronos('1 hour ago'); // 1975-12-24 23:00:00
固定をリセットするには、 ``setTestNow()`` をパラメーター無し、または ``null`` を設定して
再び呼び出してください。
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'pt'
+7
View File
@@ -0,0 +1,7 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Chronos
/index
API <https://api.cakephp.org/chronos>
+282
View File
@@ -0,0 +1,282 @@
Chronos
#######
O Chronos oferece uma coleção independente de extensões para lidar com o objeto
``DateTime``. Além de métodos de conveniência, o Chronos oferece:
* Objetos ``Date`` para representar datas de calendário.
* Objetos *date* e *datetime* imutáveis.
* Um sistema de tradução acoplável. Apenas traduções em inglês estão incluídas
na biblioteca. Todavia, ``cakephp/i18n`` pode ser usado para suporte completo
a idiomas.
Instalação
----------
Para instalar o Chronos, você deve usar o ``composer``. A partir do diretório
*ROOT* de sua aplicação (local onde o arquivo composer.json está localizado)
execute o seguinte comando::
php composer.phar require cakephp/chronos "@stable"
Visão geral
-----------
Chronos oferece extensões para lidar com objetos *DateTime* do PHP. 5 classes
cobrem variantes de data/hora mutáveis e imutáveis e uma extensão do objeto
``DateInterval``.
* ``Cake\Chronos\Chronos`` é um objeto *date & time* imutável.
* ``Cake\Chronos\ChronosDate`` é um objeto *date* imutável.
* ``Cake\Chronos\MutableDateTime`` é um objeto *date and time* mutável.
* ``Cake\Chronos\MutableDate`` é um objeto *date* mutável.
* ``Cake\Chronos\ChronosInterval`` é uma extensão do objeto ``DateInterval``.
Criando instâncias
------------------
Existem várias maneiras de criar instâncias do Chronos ou mesmo, do objeto Date.
Um número considerável de métodos padrão que funcionam com conjuntos diferentes
de argumentos::
use Cake\Chronos\Chronos;
$now = Chronos::now();
$today = Chronos::today();
$yesterday = Chronos::yesterday();
$tomorrow = Chronos::tomorrow();
// Interpreta expressões relativas.
$date = Chronos::parse('+2 days, +3 hours');
// Valores inteiros de Date e Time.
$date = Chronos::create(2015, 12, 25, 4, 32, 58);
// Valores inteiros de Date ou Time.
$date = Chronos::createFromDate(2015, 12, 25);
$date = Chronos::createFromTime(11, 45, 10);
// Interpreta valores formatados.
$date = Chronos::createFromFormat('m/d/Y', '06/15/2015');
Trabalhando com objetos imutáveis
---------------------------------
Se você é familiarizado com os objetos ``DateTime`` do PHP, você se sentirá
confortável com objetos *mutáveis*. Além de objetos mutáveis o Chronos também
oferece objetos imutáveis que por sua vez criam cópias de objetos toda vez que
um objeto é modificado. Devido ao fato de que metodos modificadores relativos
a data e hora nem sempre serem transparentes, informações podem ser modificadas
acidentalmente ou sem que o desenvolvedor saiba. Objetos imutáveis previnem
essas alterações acidentais nos dados. Imutabilidade significa que você deverá
lembrar de substituir variáveis usando modificadores::
// Esse código não funciona com objetos imutáveis
$time->addDay(1);
doSomething($time);
return $time;
// Esse funciona como o esperado
$time = $time->addDay(1);
$time = doSomething($time);
return $time;
Ao capturar o valor de retorno de cada modificação, seu código funcionará como o
esperado. Se você tem um objeto imutável e quer criar um mutável a partir do
mesmo, use ``toMutable()``::
$inplace = $time->toMutable();
Objetos Date
------------
O PHP disponibiliza um único objeto DateTime. Representar datas de calendário
pode ser um pouco desconfortável por essa classe, uma vez que ela inclui
*timezones* e componentes de hora que realmente não se encaixam no conceito de
'dia'. O Chronos oferece um objeto ``Date`` para representar datas. A hora e a
zona desse objeto é sempre fixado em ``00:00:00 UTC`` e todos os métodos de
formatação/diferença operam sob a resolução de dia::
use Cake\Chronos\ChronosDate;
$today = ChronosDate::today();
// Mudanças na hora/timezone são ignoradas
$today->modify('+1 hours');
// Exibe '2016-08-15'
echo $today;
Métodos modificadores
---------------------
Objetos Chronos disponibilizam métodos que permitem a modificação de valores de
forma granular::
// Define componentes do valor datetime
$halloween = Chronos::create()
->year(2015)
->month(10)
->day(31)
->hour(20)
->minute(30);
Você também pode modificar partes da data relativamente::
$future = Chronos::create()
->addYear(1)
->subMonth(2)
->addDays(15)
->addHours(20)
->subMinutes(2);
Também é possível realizar grandes saltos para períodos definidos no tempo::
$time = Chronos::create();
$time->startOfDay();
$time->startOfMonth();
$time->endOfMonth();
$time->endOfYear();
$time->startOfWeek();
$time->endOfWeek();
Ou ainda para dias específicos da semana::
$time->next(Chronos::TUESDAY);
$time->previous(Chronos::MONDAY);
Métodos de comparação
---------------------
Uma vez que você possui 2 instâncias de objetos data/hora do Chronos, é possível
compará-los de várias maneiras::
// Coleção completa de comparadores
// equals, notEquals, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals
$first->equals($second);
$first->greaterThanOrEquals($second);
// Veja se o objeto atual está entre outros
$now->between($start, $end);
// Encontre qual argumento está mais perto ou mais longe
$now->closest($june, $november);
$now->farthest($june, $november);
Você também pode arguir sobre quando um determinado valor cai no calendário::
$now->isToday();
$now->isYesterday();
$now->isFuture();
$now->isPast();
// Verifica se o dia é no final de semana
$now->isWeekend();
// Todos os métodos para outros dias da semana existem também
$now->isMonday();
Você também pode verificar se um determinado valor está dentro de um período de
tempo relativo::
$time->wasWithinLast('3 days');
$time->isWithinNext('3 hours');
Gerando diferenças
------------------
Em adição à comparação de *datetimes*, calcular diferenças ou deltas entre
valores é uma tarefa simples::
// Recebe um DateInterval representando a diferença
$first->diff($second);
// Recebe a diferença como um contador de unidades específicas
$first->diffInHours($second);
$first->diffInDays($second);
$first->diffInWeeks($second);
$first->diffInYears($second);
Você pode gerar diferenças de fácil leitura para humanos para usar em um *feed*
ou *timeline*::
// Diferença em relação ao momento atual
echo $date->diffForHumans();
// Diferença em relação a outro período no tempo
echo $date->diffForHumans($other); // 1 hora atrás;
Formatando strings
------------------
O Chronos disponibiliza métodos para exibir nossos objetos *datetime*::
// Usa o formato controlado por setToStringFormat()
echo $date;
// Diferentes padrões de formato
echo $time->toAtomString(); // 1975-12-25T14:15:16-05:00
echo $time->toCookieString(); // Thursday, 25-Dec-1975 14:15:16 EST
echo $time->toIso8601String(); // 1975-12-25T14:15:16-05:00
echo $time->toRfc822String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc850String(); // Thursday, 25-Dec-75 14:15:16 EST
echo $time->toRfc1036String(); // Thu, 25 Dec 75 14:15:16 -0500
echo $time->toRfc1123String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc2822String(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toRfc3339String(); // 1975-12-25T14:15:16-05:00
echo $time->toRssString(); // Thu, 25 Dec 1975 14:15:16 -0500
echo $time->toW3cString(); // 1975-12-25T14:15:16-05:00
// Recebe o trimestre
echo $time->toQuarter(); // 4;
Extraindo componentes de data
-----------------------------
Podemos receber partes de um objeto *date* acessando propriedades::
$time = new Chronos('2015-12-31 23:59:58');
$time->year; // 2015
$time->month; // 12
$time->day; // 31
$time->hour // 23
$time->minute // 59
$time->second // 58
Outras propriedades que podem ser acessadas são:
- timezone
- timezoneName
- micro
- dayOfWeek
- dayOfMonth
- dayOfYear
- daysInMonth
- timestamp
- quarter
- half
Auxílio para testes
-------------------
Ao escrever testes unitários, fixar a hora atual é bastante útil. O Chronos
lhe permite fixar a hora atual para cada classe. Como parte das suas ferramentas
de testes, você pode incluir o seguinte::
Chronos::setTestNow(Chronos::now());
MutableDateTime::setTestNow(MutableDateTime::now());
ChronosDate::setTestNow(ChronosDate::parse(Chronos::now()));
MutableDate::setTestNow(MutableDate::now());
Isso irá corrigir a hora atual de todos os objetos para o momento em que o
processo de testes foi iniciado.
Por exemplo, se você fixar o ``Chronos`` em algum momento no passado, qualquer
nova instância do ``Chronos`` criada com ``now`` ou uma *string* de tempo
relativa, teremos um retorno referente ao tempo fixado::
Chronos::setTestNow(new Chronos('1975-12-25 00:00:00'));
$time = new Chronos(); // 1975-12-25 00:00:00
$time = new Chronos('1 hour ago'); // 1975-12-24 23:00:00
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
use DatePeriod;
use Iterator;
/**
* DatePeriod wrapper that returns Chronos instances.
*
* @template TKey int
* @template TValue \Cake\Chronos\ChronosDate
* @template-implements \Iterator<int, \Cake\Chronos\ChronosDate>
*/
class ChronosDatePeriod implements Iterator
{
/**
* @var \Iterator<int, \DateTimeInterface>
*/
protected Iterator $iterator;
/**
* @param \DatePeriod $period
*/
public function __construct(DatePeriod $period)
{
/** @var \Iterator<int, \DateTimeInterface> $iterator */
$iterator = $period->getIterator();
$this->iterator = $iterator;
}
/**
* @return \Cake\Chronos\ChronosDate
*/
public function current(): ChronosDate
{
return new ChronosDate($this->iterator->current());
}
/**
* @return int
*/
public function key(): int
{
return $this->iterator->key();
}
/**
* @return void
*/
public function next(): void
{
$this->iterator->next();
}
/**
* @return void
*/
public function rewind(): void
{
$this->iterator->rewind();
}
/**
* @return bool
*/
public function valid(): bool
{
return $this->iterator->valid();
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
use DatePeriod;
use Iterator;
/**
* DatePeriod wrapper that returns Chronos instances.
*
* @template TKey int
* @template TValue \Cake\Chronos\Chronos
* @template-implements \Iterator<int, \Cake\Chronos\Chronos>
*/
class ChronosPeriod implements Iterator
{
/**
* @var \Iterator<int, \DateTimeInterface>
*/
protected Iterator $iterator;
/**
* @param \DatePeriod $period
*/
public function __construct(DatePeriod $period)
{
/** @var \Iterator<int, \DateTimeInterface> $iterator */
$iterator = $period->getIterator();
$this->iterator = $iterator;
}
/**
* @return \Cake\Chronos\Chronos
*/
public function current(): Chronos
{
return new Chronos($this->iterator->current());
}
/**
* @return int
*/
public function key(): int
{
return $this->iterator->key();
}
/**
* @return void
*/
public function next(): void
{
$this->iterator->next();
}
/**
* @return void
*/
public function rewind(): void
{
$this->iterator->rewind();
}
/**
* @return bool
*/
public function valid(): bool
{
return $this->iterator->valid();
}
}
+493
View File
@@ -0,0 +1,493 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use Stringable;
/**
* @phpstan-consistent-constructor
*/
class ChronosTime implements Stringable
{
/**
* @var int
*/
protected const TICKS_PER_MICROSECOND = 1;
/**
* @var int
*/
protected const TICKS_PER_SECOND = 1000000;
/**
* @var int
*/
protected const TICKS_PER_MINUTE = self::TICKS_PER_SECOND * 60;
/**
* @var int
*/
protected const TICKS_PER_HOUR = self::TICKS_PER_MINUTE * 60;
/**
* @var int
*/
protected const TICKS_PER_DAY = self::TICKS_PER_HOUR * 24;
/**
* Default format to use for __toString method.
*
* @var string
*/
public const DEFAULT_TO_STRING_FORMAT = 'H:i:s';
/**
* Format to use for __toString method.
*
* @var string
*/
protected static string $toStringFormat = self::DEFAULT_TO_STRING_FORMAT;
/**
* @var int
*/
protected int $ticks;
/**
* Copies time from onther instance or from time string in the format HH[:.]mm or HH[:.]mm[:.]ss.u.
*
* Defaults to server time.
*
* @param \Cake\Chronos\ChronosTime|\DateTimeInterface|string|null $time Time
* @param \DateTimeZone|string|null $timezone The timezone to use for now
*/
public function __construct(
ChronosTime|DateTimeInterface|string|null $time = null,
DateTimeZone|string|null $timezone = null,
) {
if ($time === null) {
$time = Chronos::getTestNow() ?? Chronos::now();
if ($timezone !== null) {
$time = $time->setTimezone($timezone);
}
$this->ticks = static::parseString($time->format('H:i:s.u'));
} elseif (is_string($time)) {
$this->ticks = static::parseString($time);
} elseif ($time instanceof ChronosTime) {
$this->ticks = $time->ticks;
} else {
$this->ticks = static::parseString($time->format('H:i:s.u'));
}
}
/**
* Copies time from onther instance or from string in the format HH[:.]mm or HH[:.]mm[:.]ss.u
*
* Defaults to server time.
*
* @param \Cake\Chronos\ChronosTime|\DateTimeInterface|string $time Time
* @param \DateTimeZone|string|null $timezone The timezone to use for now
* @return static
*/
public static function parse(
ChronosTime|DateTimeInterface|string|null $time = null,
DateTimeZone|string|null $timezone = null,
): static {
return new static($time, $timezone);
}
/**
* @param string $time Time string in the format HH[:.]mm or HH[:.]mm[:.]ss.u
* @return int
*/
protected static function parseString(string $time): int
{
if (!preg_match('/^\s*(\d{1,2})[:.](\d{1,2})(?|[:.](\d{1,2})[.](\d+)|[:.](\d{1,2}))?\s*$/', $time, $matches)) {
throw new InvalidArgumentException(
sprintf('Time string `%s` is not in expected format `HH[:.]mm` or `HH[:.]mm[:.]ss.u`.', $time),
);
}
$hours = (int)$matches[1];
$minutes = (int)$matches[2];
$seconds = (int)($matches[3] ?? 0);
$microseconds = (int)substr($matches[4] ?? '', 0, 6);
if ($hours > 24 || $minutes > 59 || $seconds > 59 || $microseconds > 999_999) {
throw new InvalidArgumentException(sprintf('Time string `%s` contains invalid values.', $time));
}
$ticks = $hours * self::TICKS_PER_HOUR;
$ticks += $minutes * self::TICKS_PER_MINUTE;
$ticks += $seconds * self::TICKS_PER_SECOND;
$ticks += $microseconds * self::TICKS_PER_MICROSECOND;
return $ticks % self::TICKS_PER_DAY;
}
/**
* Returns instance set to server time.
*
* @param \DateTimeZone|string|null $timezone The timezone to use for now
* @return static
*/
public static function now(DateTimeZone|string|null $timezone = null): static
{
return new static(null, $timezone);
}
/**
* Returns instance set to midnight.
*
* @return static
*/
public static function midnight(): static
{
return new static('00:00:00');
}
/**
* Returns instance set to noon.
*
* @return static
*/
public static function noon(): static
{
return new static('12:00:00');
}
/**
* Returns instance set to end of day - either
* 23:59:59 or 23:59:59.999999 if `$microseconds` is true
*
* @param bool $microseconds Whether to set microseconds or not
* @return static
*/
public static function endOfDay(bool $microseconds = false): static
{
if ($microseconds) {
return new static('23:59:59.999999');
}
return new static('23:59:59');
}
/**
* Returns clock microseconds.
*
* @return int
*/
public function getMicroseconds(): int
{
return intdiv($this->ticks % self::TICKS_PER_SECOND, self::TICKS_PER_MICROSECOND);
}
/**
* Sets clock microseconds.
*
* @param int $microseconds Clock microseconds
* @return static
*/
public function setMicroseconds(int $microseconds): static
{
$baseTicks = $this->ticks - $this->ticks % self::TICKS_PER_SECOND;
$newTicks = static::mod($baseTicks + $microseconds * self::TICKS_PER_MICROSECOND, self::TICKS_PER_DAY);
$clone = clone $this;
$clone->ticks = $newTicks;
return $clone;
}
/**
* Return clock seconds.
*
* @return int
*/
public function getSeconds(): int
{
$secondsTicks = $this->ticks % self::TICKS_PER_MINUTE - $this->ticks % self::TICKS_PER_SECOND;
return intdiv($secondsTicks, self::TICKS_PER_SECOND);
}
/**
* Set clock seconds.
*
* @param int $seconds Clock seconds
* @return static
*/
public function setSeconds(int $seconds): static
{
$baseTicks = $this->ticks - ($this->ticks % self::TICKS_PER_MINUTE - $this->ticks % self::TICKS_PER_SECOND);
$newTicks = static::mod($baseTicks + $seconds * self::TICKS_PER_SECOND, self::TICKS_PER_DAY);
$clone = clone $this;
$clone->ticks = $newTicks;
return $clone;
}
/**
* Returns clock minutes.
*
* @return int
*/
public function getMinutes(): int
{
$minutesTicks = $this->ticks % self::TICKS_PER_HOUR - $this->ticks % self::TICKS_PER_MINUTE;
return intdiv($minutesTicks, self::TICKS_PER_MINUTE);
}
/**
* Set clock minutes.
*
* @param int $minutes Clock minutes
* @return static
*/
public function setMinutes(int $minutes): static
{
$baseTicks = $this->ticks - ($this->ticks % self::TICKS_PER_HOUR - $this->ticks % self::TICKS_PER_MINUTE);
$newTicks = static::mod($baseTicks + $minutes * self::TICKS_PER_MINUTE, self::TICKS_PER_DAY);
$clone = clone $this;
$clone->ticks = $newTicks;
return $clone;
}
/**
* Returns clock hours.
*
* @return int
*/
public function getHours(): int
{
$hoursInTicks = $this->ticks - $this->ticks % self::TICKS_PER_HOUR;
return intdiv($hoursInTicks, self::TICKS_PER_HOUR);
}
/**
* Set clock hours.
*
* @param int $hours Clock hours
* @return static
*/
public function setHours(int $hours): static
{
$baseTicks = $this->ticks - ($this->ticks - $this->ticks % self::TICKS_PER_HOUR);
$newTicks = static::mod($baseTicks + $hours * self::TICKS_PER_HOUR, self::TICKS_PER_DAY);
$clone = clone $this;
$clone->ticks = $newTicks;
return $clone;
}
/**
* Sets clock time.
*
* @param int $hours Clock hours
* @param int $minutes Clock minutes
* @param int $seconds Clock seconds
* @param int $microseconds Clock microseconds
* @return static
*/
public function setTime(int $hours = 0, int $minutes = 0, int $seconds = 0, int $microseconds = 0): static
{
$ticks = $hours * self::TICKS_PER_HOUR +
$minutes * self::TICKS_PER_MINUTE +
$seconds * self::TICKS_PER_SECOND +
$microseconds * self::TICKS_PER_MICROSECOND;
$ticks = static::mod($ticks, self::TICKS_PER_DAY);
$clone = clone $this;
$clone->ticks = $ticks;
return $clone;
}
/**
* @param int $a Left side
* @param int $a Right side
* @return int
*/
protected static function mod(int $a, int $b): int
{
if ($a < 0) {
return $a % $b + $b;
}
return $a % $b;
}
/**
* Formats string using the same syntax as `DateTimeImmutable::format()`.
*
* As this uses DateTimeImmutable::format() to format the string, non-time formatters
* will still be interpreted. Be sure to escape those characters first.
*
* @param string $format Format string
* @return string
*/
public function format(string $format): string
{
return $this->toDateTimeImmutable()->format($format);
}
/**
* Reset the format used to the default when converting to a string
*
* @return void
*/
public static function resetToStringFormat(): void
{
static::setToStringFormat(static::DEFAULT_TO_STRING_FORMAT);
}
/**
* Set the default format used when converting to a string
*
* @param string $format The format to use in future __toString() calls.
* @return void
*/
public static function setToStringFormat(string $format): void
{
static::$toStringFormat = $format;
}
/**
* Format the instance as a string using the set format
*
* @return string
*/
public function __toString(): string
{
return $this->format(static::$toStringFormat);
}
/**
* Returns whether time is equal to target time.
*
* @param \Cake\Chronos\ChronosTime $target Target time
* @return bool
*/
public function equals(ChronosTime $target): bool
{
return $this->ticks === $target->ticks;
}
/**
* Returns whether time is greater than target time.
*
* @param \Cake\Chronos\ChronosTime $target Target time
* @return bool
*/
public function greaterThan(ChronosTime $target): bool
{
return $this->ticks > $target->ticks;
}
/**
* Returns whether time is greater than or equal to target time.
*
* @param \Cake\Chronos\ChronosTime $target Target time
* @return bool
*/
public function greaterThanOrEquals(ChronosTime $target): bool
{
return $this->ticks >= $target->ticks;
}
/**
* Returns whether time is less than target time.
*
* @param \Cake\Chronos\ChronosTime $target Target time
* @return bool
*/
public function lessThan(ChronosTime $target): bool
{
return $this->ticks < $target->ticks;
}
/**
* Returns whether time is less than or equal to target time.
*
* @param \Cake\Chronos\ChronosTime $target Target time
* @return bool
*/
public function lessThanOrEquals(ChronosTime $target): bool
{
return $this->ticks <= $target->ticks;
}
/**
* Returns whether time is between time range.
*
* @param \Cake\Chronos\ChronosTime $start Start of target range
* @param \Cake\Chronos\ChronosTime $end End of target range
* @param bool $equals Whether to include the beginning and end of range
* @return bool
*/
public function between(ChronosTime $start, ChronosTime $end, bool $equals = true): bool
{
if ($start->greaterThan($end)) {
[$start, $end] = [$end, $start];
}
if ($equals) {
return $this->greaterThanOrEquals($start) && $this->lessThanOrEquals($end);
}
return $this->greaterThan($start) && $this->lessThan($end);
}
/**
* Returns an `DateTimeImmutable` instance set to this clock time.
*
* @param \DateTimeZone|string|null $timezone Time zone the DateTimeImmutable instance will be in
* @return \DateTimeImmutable
*/
public function toDateTimeImmutable(DateTimeZone|string|null $timezone = null): DateTimeImmutable
{
$timezone = is_string($timezone) ? new DateTimeZone($timezone) : $timezone;
return (new DateTimeImmutable(timezone: $timezone))->setTime(
$this->getHours(),
$this->getMinutes(),
$this->getSeconds(),
$this->getMicroseconds(),
);
}
/**
* Returns an `DateTimeImmutable` instance set to this clock time.
*
* Alias of `toDateTimeImmutable()`.
*
* @param \DateTimeZone|string|null $timezone Time zone the DateTimeImmutable instance will be in
* @return \DateTimeImmutable
*/
public function toNative(DateTimeZone|string|null $timezone = null): DateTimeImmutable
{
return $this->toDateTimeImmutable($timezone);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Cake\Chronos;
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
use DateTimeImmutable;
use DateTimeZone;
use Psr\Clock\ClockInterface;
/**
* PSR-20 Clock implementation.
*/
class ClockFactory implements ClockInterface
{
private DateTimeZone|string|null $timezone;
/**
* Constructor.
*
* @param \DateTimeZone|string|null $timezone The timezone
*/
public function __construct(DateTimeZone|string|null $timezone = null)
{
$this->timezone = $timezone;
}
/**
* Returns the current time object.
*
* @return \Cake\Chronos\Chronos The current time
*/
public function now(): DateTimeImmutable
{
return Chronos::now($this->timezone);
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Brian Nesbitt <brian@nesbot.com>
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
use DateTimeInterface;
/**
* Handles formatting differences in text.
*
* Provides a swappable component for other libraries to leverage.
* when localizing or customizing the difference output.
*
* @internal
*/
class DifferenceFormatter implements DifferenceFormatterInterface
{
/**
* The text translator object
*
* @var \Cake\Chronos\Translator
*/
protected Translator $translate;
/**
* Constructor.
*
* @param \Cake\Chronos\Translator|null $translate The text translator object.
*/
public function __construct(?Translator $translate = null)
{
$this->translate = $translate ?: new Translator();
}
/**
* @inheritDoc
*/
public function diffForHumans(
ChronosDate|DateTimeInterface $first,
ChronosDate|DateTimeInterface|null $second = null,
bool $absolute = false,
): string {
$isNow = $second === null;
if ($second === null) {
if ($first instanceof ChronosDate) {
$second = new ChronosDate(Chronos::now());
} else {
$second = Chronos::now($first->getTimezone());
}
}
assert(
($first instanceof ChronosDate && $second instanceof ChronosDate) ||
($first instanceof DateTimeInterface && $second instanceof DateTimeInterface),
);
$diffInterval = $first->diff($second);
switch (true) {
case $diffInterval->y > 0:
$unit = 'year';
$count = $diffInterval->y;
break;
case $diffInterval->m >= 2:
$unit = 'month';
$count = $diffInterval->m;
break;
case $diffInterval->days >= Chronos::DAYS_PER_WEEK * 3:
$unit = 'week';
$count = (int)($diffInterval->days / Chronos::DAYS_PER_WEEK);
break;
case $diffInterval->d > 0:
$unit = 'day';
$count = $diffInterval->d;
break;
case $diffInterval->h > 0:
$unit = 'hour';
$count = $diffInterval->h;
break;
case $diffInterval->i > 0:
$unit = 'minute';
$count = $diffInterval->i;
break;
default:
$count = $diffInterval->s;
$unit = 'second';
break;
}
$time = $this->translate->plural($unit, $count, ['count' => $count]);
if ($absolute) {
return $time;
}
$isFuture = $diffInterval->invert === 1;
$transId = $isNow ? ($isFuture ? 'from_now' : 'ago') : ($isFuture ? 'after' : 'before');
// Some langs have special pluralization for past and future tense.
$tryKeyExists = $unit . '_' . $transId;
if ($this->translate->exists($tryKeyExists)) {
$time = $this->translate->plural($tryKeyExists, $count, ['count' => $count]);
}
return $this->translate->singular($transId, ['time' => $time]);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
use DateTimeInterface;
/**
* Interface for formatting differences in text.
*/
interface DifferenceFormatterInterface
{
/**
* Get the difference in a human readable format.
*
* @param \Cake\Chronos\ChronosDate|\DateTimeInterface $first The datetime to start with.
* @param \Cake\Chronos\ChronosDate|\DateTimeInterface|null $second The datetime to compare against.
* @param bool $absolute removes time difference modifiers ago, after, etc
* @return string The difference between the two days in a human readable format
*/
public function diffForHumans(
ChronosDate|DateTimeInterface $first,
ChronosDate|DateTimeInterface|null $second = null,
bool $absolute = false,
): string;
}
+287
View File
@@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Brian Nesbitt <brian@nesbot.com>
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
use DateTime;
/**
* Provides string formatting methods for datetime instances.
*
* Expects implementing classes to define static::$toStringFormat
*
* @internal
*/
trait FormattingTrait
{
/**
* Resets the __toString() format to ``DEFAULT_TO_STRING_FORMAT``.
*
* @return void
*/
public static function resetToStringFormat(): void
{
static::setToStringFormat(static::DEFAULT_TO_STRING_FORMAT);
}
/**
* Sets the __toString() format.
*
* @param string $format See ``format()`` for accepted specifiers.
* @return void
*/
public static function setToStringFormat(string $format): void
{
static::$toStringFormat = $format;
}
/**
* Returns a formatted string specified by ``setToStringFormat()``
* or the default ``DEFAULT_TO_STRING_FORMAT`` format.
*
* @return string
*/
public function __toString(): string
{
return $this->format(static::$toStringFormat);
}
/**
* Format the instance as date
*
* @return string
*/
public function toDateString(): string
{
return $this->format('Y-m-d');
}
/**
* Format the instance as a readable date
*
* @return string
*/
public function toFormattedDateString(): string
{
return $this->format('M j, Y');
}
/**
* Format the instance as time
*
* @return string
*/
public function toTimeString(): string
{
return $this->format('H:i:s');
}
/**
* Format the instance as date and time
*
* @return string
*/
public function toDateTimeString(): string
{
return $this->format('Y-m-d H:i:s');
}
/**
* Format the instance with day, date and time
*
* @return string
*/
public function toDayDateTimeString(): string
{
return $this->format('D, M j, Y g:i A');
}
/**
* Format the instance as ATOM
*
* @return string
*/
public function toAtomString(): string
{
return $this->format(DateTime::ATOM);
}
/**
* Format the instance as COOKIE
*
* @return string
*/
public function toCookieString(): string
{
return $this->format(DateTime::COOKIE);
}
/**
* Format the instance as ISO8601
*
* @return string
*/
public function toIso8601String(): string
{
return $this->format(DateTime::ATOM);
}
/**
* Format the instance as RFC822
*
* @return string
* @link https://tools.ietf.org/html/rfc822
*/
public function toRfc822String(): string
{
return $this->format(DateTime::RFC822);
}
/**
* Format the instance as RFC850
*
* @return string
* @link https://tools.ietf.org/html/rfc850
*/
public function toRfc850String(): string
{
return $this->format(DateTime::RFC850);
}
/**
* Format the instance as RFC1036
*
* @return string
* @link https://tools.ietf.org/html/rfc1036
*/
public function toRfc1036String(): string
{
return $this->format(DateTime::RFC1036);
}
/**
* Format the instance as RFC1123
*
* @return string
* @link https://tools.ietf.org/html/rfc1123
*/
public function toRfc1123String(): string
{
return $this->format(DateTime::RFC1123);
}
/**
* Format the instance as RFC2822
*
* @return string
* @link https://tools.ietf.org/html/rfc2822
*/
public function toRfc2822String(): string
{
return $this->format(DateTime::RFC2822);
}
/**
* Format the instance as RFC3339
*
* @return string
* @link https://tools.ietf.org/html/rfc3339
*/
public function toRfc3339String(): string
{
return $this->format(DateTime::RFC3339);
}
/**
* Format the instance as RSS
*
* @return string
*/
public function toRssString(): string
{
return $this->format(DateTime::RSS);
}
/**
* Format the instance as W3C
*
* @return string
*/
public function toW3cString(): string
{
return $this->format(DateTime::W3C);
}
/**
* Returns a UNIX timestamp.
*
* @return string UNIX timestamp
*/
public function toUnixString(): string
{
return $this->format('U');
}
/**
* Returns the quarter
*
* Deprecated 3.3.0: The $range parameter is deprecated. Use toQuarterRange() for quarter ranges.
*
* @param bool $range Range.
* @return array|int 1, 2, 3, or 4 quarter of year or array if $range true
*/
public function toQuarter(bool $range = false): int|array
{
$quarter = (int)ceil((int)$this->format('m') / 3);
if ($range === false) {
return $quarter;
}
trigger_error(
'Using toQuarter() with `$range=true` is deprecated. Use `toQuarterRange()` instead.',
E_USER_DEPRECATED,
);
return $this->toQuarterRange();
}
/**
* Returns the quarter range
*
* @return array{0: string, 1: string} Array with start and end date of quarter in Y-m-d format
*/
public function toQuarterRange(): array
{
/** @var int<1, 4> $quarter */
$quarter = (int)ceil((int)$this->format('m') / 3);
$year = $this->format('Y');
return match ($quarter) {
1 => [$year . '-01-01', $year . '-03-31'],
2 => [$year . '-04-01', $year . '-06-30'],
3 => [$year . '-07-01', $year . '-09-30'],
4 => [$year . '-10-01', $year . '-12-31'],
};
}
/**
* Returns ISO 8601 week number of year, weeks starting on Monday
*
* @return int ISO 8601 week number of year
*/
public function toWeek(): int
{
return (int)$this->format('W');
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;
/**
* Basic english only 'translator' for diffForHumans()
*
* @internal
*/
class Translator
{
/**
* Translation strings.
*
* @var array
*/
public static array $strings = [
'year' => '1 year',
'year_plural' => '{count} years',
'month' => '1 month',
'month_plural' => '{count} months',
'week' => '1 week',
'week_plural' => '{count} weeks',
'day' => '1 day',
'day_plural' => '{count} days',
'hour' => '1 hour',
'hour_plural' => '{count} hours',
'minute' => '1 minute',
'minute_plural' => '{count} minutes',
'second' => '1 second',
'second_plural' => '{count} seconds',
'ago' => '{time} ago',
'from_now' => '{time} from now',
'after' => '{time} after',
'before' => '{time} before',
];
/**
* Check if a translation key exists.
*
* @param string $key The key to check.
* @return bool Whether the key exists.
*/
public function exists(string $key): bool
{
return isset(static::$strings[$key]);
}
/**
* Get a plural message.
*
* @param string $key The key to use.
* @param int $count The number of items in the translation.
* @param array $vars Additional context variables.
* @return string The translated message or ''.
*/
public function plural(string $key, int $count, array $vars = []): string
{
if ($count === 1) {
return $this->singular($key, $vars);
}
return $this->singular($key . '_plural', ['count' => $count] + $vars);
}
/**
* Get a singular message.
*
* @param string $key The key to use.
* @param array $vars Additional context variables.
* @return string The translated message or ''.
*/
public function singular(string $key, array $vars = []): string
{
if (isset(static::$strings[$key])) {
$varKeys = array_keys($vars);
foreach ($varKeys as $i => $k) {
$varKeys[$i] = '{' . $k . '}';
}
return str_replace($varKeys, $vars, static::$strings[$key]);
}
return '';
}
}
+260
View File
@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
/**
* App is responsible for resource location, and path management.
*
* ### Adding paths
*
* Additional paths for Templates and Plugins are configured with Configure now. See config/app.php for an
* example. The `App.paths.plugins` and `App.paths.templates` variables are used to configure paths for plugins
* and templates respectively. All class based resources should be mapped using your application's autoloader.
*
* ### Inspecting loaded paths
*
* You can inspect the currently loaded paths using `App::classPath('Controller')` for example to see loaded
* controller paths.
*
* It is also possible to inspect paths for plugin classes, for instance, to get
* the path to a plugin's helpers you would call `App::classPath('View/Helper', 'MyPlugin')`
*
* ### Locating plugins
*
* Plugins can be located with App as well. Using Plugin::path('DebugKit') for example, will
* give you the full path to the DebugKit plugin.
*
* @link https://book.cakephp.org/5/en/core-libraries/app.html
*/
class App
{
/**
* Return the class name namespaced. This method checks if the class is defined on the
* application/plugin, otherwise try to load from the CakePHP core
*
* @param string $class Class name
* @param string $type Type of class
* @param string $suffix Class name suffix
* @return class-string|null Namespaced class name, null if the class is not found.
*/
public static function className(string $class, string $type = '', string $suffix = ''): ?string
{
if (str_contains($class, '\\')) {
return class_exists($class) ? $class : null;
}
[$plugin, $name] = pluginSplit($class);
$fullname = '\\' . str_replace('/', '\\', $type . '\\' . $name) . $suffix;
$base = $plugin ?: Configure::read('App.namespace');
if ($base !== null) {
$base = str_replace('/', '\\', rtrim($base, '\\'));
if (static::_classExistsInBase($fullname, $base)) {
/** @var class-string */
return $base . $fullname;
}
}
if ($plugin || !static::_classExistsInBase($fullname, 'Cake')) {
return null;
}
/** @var class-string */
return 'Cake' . $fullname;
}
/**
* Returns the plugin split name of a class
*
* Examples:
*
* ```
* App::shortName(
* 'SomeVendor\SomePlugin\Controller\Component\TestComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: SomeVendor/SomePlugin.Test
*
* ```
* App::shortName(
* 'SomeVendor\SomePlugin\Controller\Component\Subfolder\TestComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: SomeVendor/SomePlugin.Subfolder/Test
*
* ```
* App::shortName(
* 'Cake\Controller\Component\FlashComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: Flash
*
* @param string $class Class name
* @param string $type Type of class
* @param string $suffix Class name suffix
* @return string Plugin split name of class
*/
public static function shortName(string $class, string $type, string $suffix = ''): string
{
$class = str_replace('\\', '/', $class);
$type = '/' . $type . '/';
$pos = strrpos($class, $type);
if ($pos === false) {
return $class;
}
$pluginName = substr($class, 0, $pos);
$name = substr($class, $pos + strlen($type));
if ($suffix) {
$name = substr($name, 0, -strlen($suffix));
}
$nonPluginNamespaces = [
'Cake',
str_replace('\\', '/', (string)Configure::read('App.namespace')),
];
if (in_array($pluginName, $nonPluginNamespaces, true)) {
return $name;
}
return $pluginName . '.' . $name;
}
/**
* _classExistsInBase
*
* Test isolation wrapper
*
* @param string $name Class name.
* @param string $namespace Namespace.
* @return bool
*/
protected static function _classExistsInBase(string $name, string $namespace): bool
{
return class_exists($namespace . $name);
}
/**
* Used to read information of stored path.
*
* When called without the `$plugin` argument it will return the value of `App.paths.$type` config.
*
* Default types:
* - plugins
* - templates
* - locales
*
* Example:
*
* ```
* App::path('plugins');
* ```
*
* Will return the value of `App.paths.plugins` config.
*
* For plugins it can be used to get paths for types `templates` or `locales`.
*
* @param string $type Type of path
* @param string|null $plugin Plugin name
* @return array<int|string, string>
* @link https://book.cakephp.org/5/en/core-libraries/app.html#finding-paths-to-namespaces
*/
public static function path(string $type, ?string $plugin = null): array
{
if ($plugin === null) {
return (array)Configure::read('App.paths.' . $type);
}
return match ($type) {
'templates' => [Plugin::templatePath($plugin)],
'locales' => [Plugin::path($plugin) . 'resources' . DIRECTORY_SEPARATOR . 'locales' . DIRECTORY_SEPARATOR],
default => throw new CakeException(sprintf(
'Invalid type `%s`. Only path types `templates` and `locales` are supported for plugins.',
$type,
))
};
}
/**
* Gets the path to a class type in the application or a plugin.
*
* Example:
*
* ```
* App::classPath('Model/Table');
* ```
*
* Will return the path for tables - e.g. `src/Model/Table/`.
*
* ```
* App::classPath('Model/Table', 'My/Plugin');
* ```
*
* Will return the plugin based path for those.
*
* @param string $type Package type.
* @param string|null $plugin Plugin name.
* @return array<string>
*/
public static function classPath(string $type, ?string $plugin = null): array
{
if ($plugin !== null) {
return [
Plugin::classPath($plugin) . $type . DIRECTORY_SEPARATOR,
];
}
return [APP . $type . DIRECTORY_SEPARATOR];
}
/**
* Returns the full path to a package inside the CakePHP core
*
* Usage:
*
* ```
* App::core('Cache/Engine');
* ```
*
* Will return the full path to the cache engines package.
*
* @param string $type Package type.
* @return array<string> Full path to package
*/
public static function core(string $type): array
{
if ($type === 'templates') {
return [CORE_PATH . 'templates' . DIRECTORY_SEPARATOR];
}
return [CAKE . str_replace('/', DIRECTORY_SEPARATOR, $type) . DIRECTORY_SEPARATOR];
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Attribute;
use Attribute;
use Cake\Core\Configure as CakeConfigure;
use League\Container\Attribute\AttributeInterface;
/**
* Configure attribute for dependency injection container delegate.
*
* This provides autowiring config data into constructors when delegate is enabled.
*
* Example:
* ```
* <?php
* declare(strict_types=1);
*
* namespace App\Model\WebService;
* use Cake\Core\Attribute\Configure;
*
* class CustomClient
* {
* public function __construct(
* #[Configure('CustomService.apiKey')] protected string $apiKey,
* ) { }
* }
* ```
*/
#[Attribute(Attribute::TARGET_PARAMETER)]
class Configure implements AttributeInterface
{
/**
* @param string $name
*/
public function __construct(private string $name)
{
}
/**
* @return mixed
*/
public function resolve(): mixed
{
return CakeConfigure::read($this->name);
}
}
+324
View File
@@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Event\EventManagerInterface;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
use Closure;
use InvalidArgumentException;
use ReflectionClass;
/**
* Base Plugin Class
*
* Every plugin should extend from this class or implement the interfaces and
* include a plugin class in its src root folder.
*/
class BasePlugin implements PluginInterface
{
/**
* Do bootstrapping or not
*
* @var bool
*/
protected bool $bootstrapEnabled = true;
/**
* Console middleware
*
* @var bool
*/
protected bool $consoleEnabled = true;
/**
* Enable middleware
*
* @var bool
*/
protected bool $middlewareEnabled = true;
/**
* Register container services
*
* @var bool
*/
protected bool $servicesEnabled = true;
/**
* Load routes or not
*
* @var bool
*/
protected bool $routesEnabled = true;
/**
* Load events or not
*
* @var bool
*/
protected bool $eventsEnabled = true;
/**
* The path to this plugin.
*
* @var string|null
*/
protected ?string $path = null;
/**
* The class path for this plugin.
*
* @var string|null
*/
protected ?string $classPath = null;
/**
* The config path for this plugin.
*
* @var string|null
*/
protected ?string $configPath = null;
/**
* The templates path for this plugin.
*
* @var string|null
*/
protected ?string $templatePath = null;
/**
* The name of this plugin
*
* @var string|null
*/
protected ?string $name = null;
/**
* Constructor
*
* @param array<string, mixed> $options Options
*/
public function __construct(array $options = [])
{
foreach (static::VALID_HOOKS as $key) {
if (isset($options[$key])) {
$this->{"{$key}Enabled"} = (bool)$options[$key];
}
}
foreach (['name', 'path', 'classPath', 'configPath', 'templatePath'] as $path) {
if (isset($options[$path])) {
$this->{$path} = $options[$path];
}
}
$this->initialize();
}
/**
* Initialization hook called from constructor.
*
* @return void
*/
public function initialize(): void
{
}
/**
* @inheritDoc
*/
public function getName(): string
{
if ($this->name !== null) {
return $this->name;
}
$parts = explode('\\', static::class);
array_pop($parts);
return $this->name = implode('/', $parts);
}
/**
* @inheritDoc
*/
public function getPath(): string
{
if ($this->path !== null) {
return $this->path;
}
$reflection = new ReflectionClass($this);
$path = dirname((string)$reflection->getFileName());
// Trim off src
if (str_ends_with($path, 'src')) {
$path = substr($path, 0, -3);
}
return $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getConfigPath(): string
{
if ($this->configPath !== null) {
return $this->configPath;
}
$path = $this->getPath();
return $path . 'config' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getClassPath(): string
{
if ($this->classPath !== null) {
return $this->classPath;
}
$path = $this->getPath();
return $path . 'src' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getTemplatePath(): string
{
if ($this->templatePath !== null) {
return $this->templatePath;
}
$path = $this->getPath();
return $this->templatePath = $path . 'templates' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function enable(string $hook)
{
$this->checkHook($hook);
$this->{"{$hook}Enabled"} = true;
return $this;
}
/**
* @inheritDoc
*/
public function disable(string $hook)
{
$this->checkHook($hook);
$this->{"{$hook}Enabled"} = false;
return $this;
}
/**
* @inheritDoc
*/
public function isEnabled(string $hook): bool
{
$this->checkHook($hook);
return $this->{"{$hook}Enabled"} === true;
}
/**
* Check if a hook name is valid
*
* @param string $hook The hook name to check
* @throws \InvalidArgumentException on invalid hooks
* @return void
*/
protected function checkHook(string $hook): void
{
if (!in_array($hook, static::VALID_HOOKS, true)) {
throw new InvalidArgumentException(sprintf(
'`%s` is not a valid hook name. Must be one of `%s.`',
$hook,
implode(', ', static::VALID_HOOKS),
));
}
}
/**
* @inheritDoc
*/
public function routes(RouteBuilder $routes): void
{
$path = $this->getConfigPath() . 'routes.php';
if (is_file($path)) {
$return = require $path;
if ($return instanceof Closure) {
$return($routes);
}
}
}
/**
* @inheritDoc
*/
public function bootstrap(PluginApplicationInterface $app): void
{
$bootstrap = $this->getConfigPath() . 'bootstrap.php';
if (is_file($bootstrap)) {
require $bootstrap;
}
}
/**
* @inheritDoc
*/
public function console(CommandCollection $commands): CommandCollection
{
return $commands->addMany($commands->discoverPlugin($this->getName()));
}
/**
* @inheritDoc
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
return $middlewareQueue;
}
/**
* Register container services for this plugin.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
public function services(ContainerInterface $container): void
{
}
/**
* Register application events.
*
* @param \Cake\Event\EventManagerInterface $eventManager The global event manager to register listeners on
* @return \Cake\Event\EventManagerInterface
*/
public function events(EventManagerInterface $eventManager): EventManagerInterface
{
return $eventManager;
}
}
+493
View File
@@ -0,0 +1,493 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Cache\Cache;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
/**
* Configuration class. Used for managing runtime configuration information.
*
* Provides features for reading and writing to the runtime configuration, as well
* as methods for loading additional configuration files or storing runtime configuration
* for future use.
*
* @link https://book.cakephp.org/5/en/development/configuration.html
*/
class Configure
{
/**
* Array of values currently stored in Configure.
*
* @var array<string, mixed>
*/
protected static array $_values = [
'debug' => false,
];
/**
* Configured engine classes, used to load config files from resources
*
* @see \Cake\Core\Configure::load()
* @var array<\Cake\Core\Configure\ConfigEngineInterface>
*/
protected static array $_engines = [];
/**
* Flag to track whether ini_set exists.
*
* @var bool|null
*/
protected static ?bool $_hasIniSet = null;
/**
* Used to store a dynamic variable in Configure.
*
* Usage:
* ```
* Configure::write('One.key1', 'value of the Configure::One[key1]');
* Configure::write(['One.key1' => 'value of the Configure::One[key1]']);
* Configure::write('One', [
* 'key1' => 'value of the Configure::One[key1]',
* 'key2' => 'value of the Configure::One[key2]'
* ]);
*
* Configure::write([
* 'One.key1' => 'value of the Configure::One[key1]',
* 'One.key2' => 'value of the Configure::One[key2]'
* ]);
* ```
*
* @param array<string, mixed>|string $config The key to write, can be a dot notation value.
* Alternatively can be an array containing key(s) and value(s).
* @param mixed $value Value to set for the given key.
* @return void
* @link https://book.cakephp.org/5/en/development/configuration.html#writing-configuration-data
*/
public static function write(array|string $config, mixed $value = null): void
{
if (!is_array($config)) {
$config = [$config => $value];
}
foreach ($config as $name => $valueToInsert) {
static::$_values = Hash::insert(static::$_values, $name, $valueToInsert);
}
if (isset($config['debug'])) {
static::$_hasIniSet ??= function_exists('ini_set');
if (static::$_hasIniSet) {
ini_set('display_errors', $config['debug'] ? '1' : '0');
}
}
}
/**
* Used to read information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Usage:
* ```
* Configure::read('Name'); will return all values for Name
* Configure::read('Name.key'); will return only the value of Configure::Name[key]
* ```
*
* @param string|null $var Variable to obtain. Use '.' to access array elements.
* @param mixed $default The return value when the configure does not exist
* @return mixed Value stored in configure, or null.
* @link https://book.cakephp.org/5/en/development/configuration.html#reading-configuration-data
*/
public static function read(?string $var = null, mixed $default = null): mixed
{
if ($var === null) {
return static::$_values;
}
return Hash::get(static::$_values, $var, $default);
}
/**
* Returns true if given variable is set in Configure.
*
* @param string $var Variable name to check for
* @return bool True if variable is there
*/
public static function check(string $var): bool
{
if (!$var) {
return false;
}
return static::read($var) !== null;
}
/**
* Used to get information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Acts as a wrapper around Configure::read() and Configure::check().
* The configure key/value pair fetched via this method is expected to exist.
* In case it does not an exception will be thrown.
*
* Usage:
* ```
* Configure::readOrFail('Name'); will return all values for Name
* Configure::readOrFail('Name.key'); will return only the value of Configure::Name[key]
* ```
*
* @param string $var Variable to obtain. Use '.' to access array elements.
* @return mixed Value stored in configure.
* @throws \Cake\Core\Exception\CakeException if the requested configuration is not set.
* @link https://book.cakephp.org/5/en/development/configuration.html#reading-configuration-data
*/
public static function readOrFail(string $var): mixed
{
if (!static::check($var)) {
throw new CakeException(sprintf('Expected configuration key `%s` not found.', $var));
}
return static::read($var);
}
/**
* Used to delete a variable from Configure.
*
* Usage:
* ```
* Configure::delete('Name'); will delete the entire Configure::Name
* Configure::delete('Name.key'); will delete only the Configure::Name[key]
* ```
*
* @param string $var the var to be deleted
* @return void
* @link https://book.cakephp.org/5/en/development/configuration.html#deleting-configuration-data
*/
public static function delete(string $var): void
{
static::$_values = Hash::remove(static::$_values, $var);
}
/**
* Used to consume information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Acts as a wrapper around Configure::consume() and Configure::check().
* The configure key/value pair consumed via this method is expected to exist.
* In case it does not an exception will be thrown.
*
* @param string $var Variable to consume. Use '.' to access array elements.
* @return mixed Value stored in configure.
* @throws \Cake\Core\Exception\CakeException if the requested configuration is not set.
* @since 3.6.0
*/
public static function consumeOrFail(string $var): mixed
{
if (!static::check($var)) {
throw new CakeException(sprintf('Expected configuration key `%s` not found.', $var));
}
return static::consume($var);
}
/**
* Used to read and delete a variable from Configure.
*
* This is primarily used during bootstrapping to move configuration data
* out of configure into the various other classes in CakePHP.
*
* @param string $var The key to read and remove.
* @return mixed The value stored in Configure, or null if the key doesn't exist.
*/
public static function consume(string $var): mixed
{
if (!str_contains($var, '.')) {
if (!isset(static::$_values[$var])) {
return null;
}
$value = static::$_values[$var];
unset(static::$_values[$var]);
return $value;
}
$value = Hash::get(static::$_values, $var);
static::delete($var);
return $value;
}
/**
* Add a new engine to Configure. Engines allow you to read configuration
* files in various formats/storage locations. CakePHP comes with two built-in engines
* PhpConfig and IniConfig. You can also implement your own engine classes in your application.
*
* To add a new engine to Configure:
*
* ```
* Configure::config('ini', new IniConfig());
* ```
*
* @param string $name The name of the engine being configured. This alias is used later to
* read values from a specific engine.
* @param \Cake\Core\Configure\ConfigEngineInterface $engine The engine to append.
* @return void
*/
public static function config(string $name, ConfigEngineInterface $engine): void
{
static::$_engines[$name] = $engine;
}
/**
* Returns true if the Engine objects is configured.
*
* @param string $name Engine name.
* @return bool
*/
public static function isConfigured(string $name): bool
{
return isset(static::$_engines[$name]);
}
/**
* Gets the names of the configured Engine objects.
*
* @return array<string>
*/
public static function configured(): array
{
$engines = array_keys(static::$_engines);
return array_map(function (int|string $key) {
return (string)$key;
}, $engines);
}
/**
* Remove a configured engine. This will unset the engine
* and make any future attempts to use it cause an Exception.
*
* @param string $name Name of the engine to drop.
* @return bool Success
*/
public static function drop(string $name): bool
{
if (!isset(static::$_engines[$name])) {
return false;
}
unset(static::$_engines[$name]);
return true;
}
/**
* Loads stored configuration information from a resource. You can add
* config file resource engines with `Configure::config()`.
*
* Loaded configuration information will be merged with the current
* runtime configuration. You can load configuration files from plugins
* by preceding the filename with the plugin name.
*
* `Configure::load('Users.user', 'default')`
*
* Would load the 'user' config file using the default config engine. You can load
* app config files by giving the name of the resource you want loaded.
*
* ```
* Configure::load('setup', 'default');
* ```
*
* If using `default` config and no engine has been configured for it yet,
* one will be automatically created using PhpConfig
*
* @param string $key name of configuration resource to load.
* @param string $config Name of the configured engine to use to read the resource identified by $key.
* @param bool $merge if config files should be merged instead of simply overridden
* @return bool True if load successful.
* @throws \Cake\Core\Exception\CakeException if the $config engine is not found
* @link https://book.cakephp.org/5/en/development/configuration.html#reading-and-writing-configuration-files
*/
public static function load(string $key, string $config = 'default', bool $merge = true): bool
{
$engine = static::_getEngine($config);
if (!$engine) {
throw new CakeException(
sprintf(
'Config %s engine not found when attempting to load %s.',
$config,
$key,
),
);
}
$values = $engine->read($key);
if ($merge) {
$values = Hash::merge(static::$_values, $values);
}
static::write($values);
return true;
}
/**
* Dump data currently in Configure into $key. The serialization format
* is decided by the config engine attached as $config. For example, if the
* 'default' adapter is a PhpConfig, the generated file will be a PHP
* configuration file loadable by the PhpConfig.
*
* ### Usage
*
* Given that the 'default' engine is an instance of PhpConfig.
* Save all data in Configure to the file `my_config.php`:
*
* ```
* Configure::dump('my_config', 'default');
* ```
*
* Save only the error handling configuration:
*
* ```
* Configure::dump('error', 'default', ['Error', 'Exception'];
* ```
*
* @param string $key The identifier to create in the config adapter.
* This could be a filename or a cache key depending on the adapter being used.
* @param string $config The name of the configured adapter to dump data with.
* @param array<string> $keys The name of the top-level keys you want to dump.
* This allows you save only some data stored in Configure.
* @return bool Success
* @throws \Cake\Core\Exception\CakeException if the adapter does not implement a `dump` method.
*/
public static function dump(string $key, string $config = 'default', array $keys = []): bool
{
$engine = static::_getEngine($config);
if (!$engine) {
throw new CakeException(sprintf('There is no `%s` config engine.', $config));
}
$values = static::$_values;
if ($keys) {
$values = array_intersect_key($values, array_flip($keys));
}
return $engine->dump($key, $values);
}
/**
* Get the configured engine. Internally used by `Configure::load()` and `Configure::dump()`
* Will create new PhpConfig for default if not configured yet.
*
* @param string $config The name of the configured adapter
* @return \Cake\Core\Configure\ConfigEngineInterface|null Engine instance or null
*/
protected static function _getEngine(string $config): ?ConfigEngineInterface
{
if (!isset(static::$_engines[$config])) {
if ($config !== 'default') {
return null;
}
static::config($config, new PhpConfig());
}
return static::$_engines[$config];
}
/**
* Used to determine the current version of CakePHP.
*
* Usage
* ```
* Configure::version();
* ```
*
* @return string Current version of CakePHP
*/
public static function version(): string
{
$version = static::read('Cake.version');
if ($version !== null) {
return $version;
}
$path = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config/config.php';
if (is_file($path)) {
$config = require $path;
static::write($config);
return static::read('Cake.version');
}
return 'unknown';
}
/**
* Used to write runtime configuration into Cache. Stored runtime configuration can be
* restored using `Configure::restore()`. These methods can be used to enable configuration managers
* frontends, or other GUI type interfaces for configuration.
*
* @param string $name The storage name for the saved configuration.
* @param string $cacheConfig The cache configuration to save into. Defaults to 'default'
* @param array|null $data Either an array of data to store, or leave empty to store all values.
* @return bool Success
*/
public static function store(string $name, string $cacheConfig = 'default', ?array $data = null): bool
{
$data ??= static::$_values;
if (!class_exists(Cache::class)) {
throw new CakeException('You must install cakephp/cache to use Configure::store()');
}
return Cache::write($name, $data, $cacheConfig);
}
/**
* Restores configuration data stored in the Cache into configure. Restored
* values will overwrite existing ones.
*
* @param string $name Name of the stored config file to load.
* @param string $cacheConfig Name of the Cache configuration to read from.
* @return bool Success.
*/
public static function restore(string $name, string $cacheConfig = 'default'): bool
{
if (!class_exists(Cache::class)) {
throw new CakeException('You must install cakephp/cache to use Configure::restore()');
}
$values = Cache::read($name, $cacheConfig);
if ($values) {
static::write($values);
return true;
}
return false;
}
/**
* Clear all values stored in Configure.
*
* @return void
*/
public static function clear(): void
{
static::$_values = [];
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure;
/**
* An interface for creating objects compatible with Configure::load()
*/
interface ConfigEngineInterface
{
/**
* Read a configuration file/storage key
*
* This method is used for reading configuration information from sources.
* These sources can either be static resources like files, or dynamic ones like
* a database, or other datasource.
*
* @param string $key Key to read.
* @return array An array of data to merge into the runtime configuration
*/
public function read(string $key): array;
/**
* Dumps the configure data into the storage key/file of the given `$key`.
*
* @param string $key The identifier to write to.
* @param array $data The data to dump.
* @return bool True on success or false on failure.
*/
public function dump(string $key, array $data): bool;
}
@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
/**
* Ini file configuration engine.
*
* Since IniConfig uses parse_ini_file underneath, you should be aware that this
* class shares the same behavior, especially with regards to boolean and null values.
*
* In addition to the native `parse_ini_file` features, IniConfig also allows you
* to create nested array structures through usage of `.` delimited names. This allows
* you to create nested arrays structures in an ini config file. For example:
*
* `db.password = secret` would turn into `['db' => ['password' => 'secret']]`
*
* You can nest properties as deeply as needed using `.`'s. In addition to using `.` you
* can use standard ini section notation to create nested structures:
*
* ```
* [section]
* key = value
* ```
*
* Once loaded into Configure, the above would be accessed using:
*
* `Configure::read('section.key');`
*
* You can also use `.` separated values in section names to create more deeply
* nested structures.
*
* IniConfig also manipulates how the special ini values of
* 'yes', 'no', 'on', 'off', 'null' are handled. These values will be
* converted to their boolean equivalents.
*
* @see https://secure.php.net/parse_ini_file
*/
class IniConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected string $_extension = '.ini';
/**
* The section to read, if null all sections will be read.
*
* @var string|null
*/
protected ?string $_section = null;
/**
* Build and construct a new ini file parser. The parser can be used to read
* ini files that are on the filesystem.
*
* @param string|null $path Path to load ini config files from. Defaults to CONFIG.
* @param string|null $section Only get one section, leave null to parse and fetch
* all sections in the ini file.
*/
public function __construct(?string $path = null, ?string $section = null)
{
$this->_path = $path ?? CONFIG;
$this->_section = $section;
}
/**
* Read an ini file and return the results as an array.
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix. The chosen file must be on the engine's path.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException when files don't exist.
* Or when files contain '..' as this could lead to abusive reads.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$contents = parse_ini_file($file, true);
if ($contents === false) {
throw new CakeException(sprintf('Cannot parse INI file `%s`', $file));
}
if ($this->_section && isset($contents[$this->_section])) {
$values = $this->_parseNestedValues($contents[$this->_section]);
} else {
$values = [];
foreach ($contents as $section => $attribs) {
if (is_array($attribs)) {
$values[$section] = $this->_parseNestedValues($attribs);
} else {
$parse = $this->_parseNestedValues([$attribs]);
$values[$section] = array_shift($parse);
}
}
}
return $values;
}
/**
* parses nested values out of keys.
*
* @param array $values Values to be exploded.
* @return array Array of values exploded
*/
protected function _parseNestedValues(array $values): array
{
foreach ($values as $key => $value) {
if ($value === '1') {
$value = true;
}
if ($value === '') {
$value = false;
}
unset($values[$key]);
if (str_contains((string)$key, '.')) {
$values = Hash::insert($values, $key, $value);
} else {
$values[$key] = $value;
}
}
return $values;
}
/**
* Dumps the state of Configure data into an ini formatted string.
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param array $data The data to convert to ini file.
* @return bool Success.
*/
public function dump(string $key, array $data): bool
{
$result = [];
foreach ($data as $k => $value) {
$isSection = false;
if (!str_starts_with($k, '[')) {
$result[] = "[{$k}]";
$isSection = true;
}
if (is_array($value)) {
$kValues = Hash::flatten($value, '.');
foreach ($kValues as $k2 => $v) {
$result[] = "{$k2} = " . $this->_value($v);
}
}
if ($isSection) {
$result[] = '';
}
}
$contents = trim(implode("\n", $result));
$filename = $this->_getFilePath($key);
return file_put_contents($filename, $contents) > 0;
}
/**
* Converts a value into the ini equivalent
*
* @param mixed $value Value to export.
* @return string String value for ini file.
*/
protected function _value(mixed $value): string
{
return match ($value) {
null => 'null',
true => 'true',
false => 'false',
default => (string)$value
};
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
/**
* JSON engine allows Configure to load configuration values from
* files containing JSON strings.
*
* An example JSON file would look like::
*
* ```
* {
* "debug": false,
* "App": {
* "namespace": "MyApp"
* },
* "Security": {
* "salt": "its-secret"
* }
* }
* ```
*/
class JsonConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected string $_extension = '.json';
/**
* Constructor for JSON Config file reading.
*
* @param string|null $path The path to read config files from. Defaults to CONFIG.
*/
public function __construct(?string $path = null)
{
$this->_path = $path ?? CONFIG;
}
/**
* Read a config file and return its contents.
*
* Files with `.` in the name will be treated as values in plugins. Instead of
* reading from the initialized path, plugin keys will be located using Plugin::path().
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException When files don't exist or when
* files contain '..' (as this could lead to abusive reads) or when there
* is an error parsing the JSON string.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$jsonContent = file_get_contents($file);
if ($jsonContent === false) {
throw new CakeException(sprintf('Cannot read file content of `%s`', $file));
}
$values = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing JSON string fetched from config file `%s.json`: %s',
$key,
json_last_error_msg(),
));
}
if (!is_array($values)) {
throw new CakeException(sprintf(
'Decoding JSON config file `%s.json` did not return an array',
$key,
));
}
return $values;
}
/**
* Converts the provided $data into a JSON string that can be used saved
* into a file and loaded later.
*
* @param string $key The identifier to write to. If the key has a . it will
* be treated as a plugin prefix.
* @param array $data Data to dump.
* @return bool Success
*/
public function dump(string $key, array $data): bool
{
$filename = $this->_getFilePath($key);
return file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT)) !== false;
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
/**
* PHP engine allows Configure to load configuration values from
* files containing simple PHP arrays.
*
* Files compatible with PhpConfig should return an array that
* contains all the configuration data contained in the file.
*
* An example configuration file would look like::
*
* ```
* <?php
* return [
* 'debug' => false,
* 'Security' => [
* 'salt' => 'its-secret'
* ],
* 'App' => [
* 'namespace' => 'App'
* ]
* ];
* ```
*
* @see \Cake\Core\Configure::load() for how to load custom configuration files.
*/
class PhpConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected string $_extension = '.php';
/**
* Constructor for PHP Config file reading.
*
* @param string|null $path The path to read config files from. Defaults to CONFIG.
*/
public function __construct(?string $path = null)
{
$this->_path = $path ?? CONFIG;
}
/**
* Read a config file and return its contents.
*
* Files with `.` in the name will be treated as values in plugins. Instead of
* reading from the initialized path, plugin keys will be located using Plugin::path().
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException when files don't exist or they don't contain `$config`.
* Or when files contain '..' as this could lead to abusive reads.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$return = include $file;
if (is_array($return)) {
return $return;
}
throw new CakeException(sprintf('Config file `%s` did not return an array', $key . '.php.'));
}
/**
* Converts the provided $data into a string of PHP code that can
* be used saved into a file and loaded later.
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param array $data Data to dump.
* @return bool Success
*/
public function dump(string $key, array $data): bool
{
$contents = '<?php' . "\n" . 'return ' . var_export($data, true) . ';';
$filename = $this->_getFilePath($key);
return file_put_contents($filename, $contents) > 0;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Core\Plugin;
use function Cake\Core\pluginSplit;
/**
* Trait providing utility methods for file based config engines.
*/
trait FileConfigTrait
{
/**
* The path this engine finds files on.
*
* @var string
*/
protected string $_path = '';
/**
* Get file path
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param bool $checkExists Whether to check if file exists. Defaults to false.
* @return string Full file path
* @throws \Cake\Core\Exception\CakeException When files don't exist or when
* files contain '..' as this could lead to abusive reads.
*/
protected function _getFilePath(string $key, bool $checkExists = false): string
{
if (str_contains($key, '..')) {
throw new CakeException('Cannot load/dump configuration files with ../ in them.');
}
[$plugin, $key] = pluginSplit($key);
if ($plugin) {
$file = Plugin::configPath($plugin) . $key;
} else {
$file = $this->_path . $key;
}
$file .= $this->_extension;
if (!$checkExists || is_file($file)) {
return $file;
}
$realPath = realpath($file);
if ($realPath !== false && is_file($realPath)) {
return $realPath;
}
throw new CakeException(sprintf('Could not load configuration file: `%s`.', $file));
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
/**
* An interface defining the methods that the
* console runner depend on.
*/
interface ConsoleApplicationInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* Override this method to add additional bootstrap logic for your application.
*
* @return void
*/
public function bootstrap(): void;
/**
* Define the console commands for an application.
*
* @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
* @return \Cake\Console\CommandCollection The updated collection.
*/
public function console(CommandCollection $commands): CommandCollection;
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\Container as LeagueContainer;
/**
* Dependency Injection container
*
* Based on the container out of League\Container
*/
class Container extends LeagueContainer implements ContainerInterface
{
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Interface for applications that configure and use a dependency injection container.
*/
interface ContainerApplicationInterface
{
/**
* Register services to the container
*
* Registered services can have instances fetched out of the container
* using `get()`. Dependencies and parameters will be resolved based
* on service definitions.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to
* @return void
*/
public function services(ContainerInterface $container): void;
/**
* Create a new container and register services.
*
* This will `register()` services provided by both the application
* and any plugins if the application has plugin support.
*
* @return \Cake\Core\ContainerInterface A populated container
*/
public function getContainer(): ContainerInterface;
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\DefinitionContainerInterface;
use Psr\Container\ContainerInterface as PsrContainerInterface;
/**
* Interface for the Dependency Injection Container in CakePHP applications
*
* This interface extends the PSR-11 container interface and adds
* methods to add services and service providers to the container.
*
* The methods defined in this interface use the conventions provided
* by league/container as that is the library that CakePHP uses.
*/
interface ContainerInterface extends DefinitionContainerInterface
{
/**
* @param \Psr\Container\ContainerInterface $container The container instance to use as delegation
* @return \Psr\Container\ContainerInterface
*/
public function delegate(PsrContainerInterface $container): PsrContainerInterface;
}
+156
View File
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Utility\Inflector;
/**
* Provides methods that allow other classes access to conventions based inflections.
*/
trait ConventionsTrait
{
/**
* Creates a fixture name
*
* @param string $name Model class name
* @return string Singular model key
*/
protected function _fixtureName(string $name): string
{
return Inflector::camelize($name);
}
/**
* Creates the proper entity name (singular) for the specified name
*
* @param string $name Name
* @return string Camelized and plural model name
*/
protected function _entityName(string $name): string
{
return Inflector::singularize(Inflector::camelize($name));
}
/**
* Creates the proper underscored model key for associations
*
* If the input contains a dot, assume that the right side is the real table name.
*
* @param string $name Model class name
* @return string Singular model key
*/
protected function _modelKey(string $name): string
{
[, $name] = pluginSplit($name);
return Inflector::underscore(Inflector::singularize($name)) . '_id';
}
/**
* Creates the proper model name from a foreign key
*
* @param string $key Foreign key
* @return string Model name
*/
protected function _modelNameFromKey(string $key): string
{
$key = str_replace('_id', '', $key);
return Inflector::camelize(Inflector::pluralize($key));
}
/**
* Creates the singular name for use in views.
*
* @param string $name Name to use
* @return string Variable name
*/
protected function _singularName(string $name): string
{
return Inflector::variable(Inflector::singularize($name));
}
/**
* Creates the plural variable name for views
*
* @param string $name Name to use
* @return string Plural name for views
*/
protected function _variableName(string $name): string
{
return Inflector::variable($name);
}
/**
* Creates the singular human name used in views
*
* @param string $name Controller name
* @return string Singular human name
*/
protected function _singularHumanName(string $name): string
{
return Inflector::humanize(Inflector::underscore(Inflector::singularize($name)));
}
/**
* Creates a camelized version of $name
*
* @param string $name name
* @return string Camelized name
*/
protected function _camelize(string $name): string
{
return Inflector::camelize($name);
}
/**
* Creates the plural human name used in views
*
* @param string $name Controller name
* @return string Plural human name
*/
protected function _pluralHumanName(string $name): string
{
return Inflector::humanize(Inflector::underscore($name));
}
/**
* Find the correct path for a plugin. Scans $pluginPaths for the plugin you want.
*
* @param string $pluginName Name of the plugin you want ie. DebugKit
* @return string Path to the correct plugin.
*/
protected function _pluginPath(string $pluginName): string
{
if (Plugin::isLoaded($pluginName)) {
return Plugin::path($pluginName);
}
return current(App::path('plugins')) . $pluginName . DIRECTORY_SEPARATOR;
}
/**
* Return plugin's namespace
*
* @param string $pluginName Plugin name
* @return string Plugin's namespace
*/
protected function _pluginNamespace(string $pluginName): string
{
return str_replace('/', '\\', $pluginName);
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Event\EventManagerInterface;
interface EventAwareApplicationInterface
{
/**
* Register application events.
*
* @param \Cake\Event\EventManagerInterface $eventManager The global event manager to register listeners on
* @return \Cake\Event\EventManagerInterface
*/
public function events(EventManagerInterface $eventManager): EventManagerInterface;
/**
* @param \Cake\Event\EventManagerInterface $eventManager The global event manager to register listeners on
* @return \Cake\Event\EventManagerInterface
*/
public function pluginEvents(EventManagerInterface $eventManager): EventManagerInterface;
}
+76
View File
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
use RuntimeException;
use Throwable;
/**
* Base class that all CakePHP Exceptions extend.
*/
class CakeException extends RuntimeException
{
/**
* Array of attributes that are passed in from the constructor, and
* made available in the view when a development error is displayed.
*
* @var array
*/
protected array $_attributes = [];
/**
* Template string that has attributes sprintf()'ed into it.
*
* @var string
*/
protected string $_messageTemplate = '';
/**
* Default exception code
*
* @var int
*/
protected int $_defaultCode = 0;
/**
* Constructor.
*
* Allows you to create exceptions that are treated as framework errors and disabled
* when debug mode is off.
*
* @param array|string $message Either the string of the error message, or an array of attributes
* that are made available in the view, and sprintf()'d into Exception::$_messageTemplate
* @param int|null $code The error code
* @param \Throwable|null $previous the previous exception.
*/
public function __construct(array|string $message = '', ?int $code = null, ?Throwable $previous = null)
{
if (is_array($message)) {
$this->_attributes = $message;
$message = vsprintf($this->_messageTemplate, $message);
}
parent::__construct($message, $code ?? $this->_defaultCode, $previous);
}
/**
* Get the passed in attributes
*
* @return array
*/
public function getAttributes(): array
{
return $this->_attributes;
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 5.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
use Throwable;
interface HttpErrorCodeInterface extends Throwable
{
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
/**
* Exception raised when a plugin could not be found
*/
class MissingPluginException extends CakeException
{
/**
* @inheritDoc
*/
protected string $_messageTemplate = 'Plugin `%s` could not be found.';
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Http\MiddlewareQueue;
use Psr\Http\Server\RequestHandlerInterface;
/**
* An interface defining the methods that the
* http server depend on.
*/
interface HttpApplicationInterface extends RequestHandlerInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* Override this method to add additional bootstrap logic for your application.
*
* @return void
*/
public function bootstrap(): void;
/**
* Define the HTTP middleware layers for an application.
*
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to set in your App Class
* @return \Cake\Http\MiddlewareQueue
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
}
+326
View File
@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
use InvalidArgumentException;
/**
* A trait for reading and writing instance config
*
* Implementing objects are expected to declare a `$_defaultConfig` property.
*/
trait InstanceConfigTrait
{
/**
* Runtime config
*
* @var array<string, mixed>
*/
protected array $_config = [];
/**
* Whether the config property has already been configured with defaults
*
* @var bool
*/
protected bool $_configInitialized = false;
/**
* Sets the config.
*
* ### Usage
*
* Setting a specific value:
*
* ```
* $this->setConfig('key', $value);
* ```
*
* Setting a nested value:
*
* ```
* $this->setConfig('some.nested.key', $value);
* ```
*
* Updating multiple config settings at the same time:
*
* ```
* $this->setConfig(['one' => 'value', 'another' => 'value']);
* ```
*
* @param array<string, mixed>|string $key The key to set, or a complete array of configs.
* @param mixed|null $value The value to set.
* @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
* @return $this
* @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid.
*/
public function setConfig(array|string $key, mixed $value = null, bool $merge = true)
{
$this->initCfg();
$this->_configWrite($key, $value, $merge);
return $this;
}
/**
* Returns the config.
*
* ### Usage
*
* Reading the whole config:
*
* ```
* $this->getConfig();
* ```
*
* Reading a specific value:
*
* ```
* $this->getConfig('key');
* ```
*
* Reading a nested value:
*
* ```
* $this->getConfig('some.nested.key');
* ```
*
* Reading with default value:
*
* ```
* $this->getConfig('some-key', 'default-value');
* ```
*
* @param string|null $key The key to get or null for the whole config.
* @param mixed $default The return value when the key does not exist.
* @return ($key is null ? array : mixed) Configuration data at the named key or null if the key does not exist.
*/
public function getConfig(?string $key = null, mixed $default = null): mixed
{
$this->initCfg();
return $this->_configRead($key) ?? $default;
}
/**
* Returns the config for this specific key.
*
* The config value for this key must exist, it can never be null.
*
* @param string $key The key to get.
* @return mixed Configuration data at the named key
* @throws \InvalidArgumentException
*/
public function getConfigOrFail(string $key): mixed
{
$config = $this->getConfig($key);
if ($config === null) {
throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
}
return $config;
}
/**
* Merge provided config with existing config. Unlike `config()` which does
* a recursive merge for nested keys, this method does a simple merge.
*
* Setting a specific value:
*
* ```
* $this->configShallow('key', $value);
* ```
*
* Setting a nested value:
*
* ```
* $this->configShallow('some.nested.key', $value);
* ```
*
* Updating multiple config settings at the same time:
*
* ```
* $this->configShallow(['one' => 'value', 'another' => 'value']);
* ```
*
* @param array<string, mixed>|string $key The key to set, or a complete array of configs.
* @param mixed|null $value The value to set.
* @return $this
*/
public function configShallow(array|string $key, mixed $value = null)
{
$this->initCfg();
$this->_configWrite($key, $value, 'shallow');
return $this;
}
/**
* Deletes a config key.
*
* @param string $key Key to delete. It can be a dot separated string to delete nested keys.
* @return $this
*/
public function deleteConfig(string $key)
{
$this->initCfg();
$this->_configDelete($key);
return $this;
}
/**
* Initializes the config with the default config.
*
* @return void
*/
private function initCfg(): void
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
}
/**
* Reads a config key.
*
* @param string|null $key Key to read.
* @return ($key is null ? array : mixed)
*/
protected function _configRead(?string $key): mixed
{
if ($key === null) {
return $this->_config;
}
if (!str_contains($key, '.')) {
return $this->_config[$key] ?? null;
}
$return = $this->_config;
foreach (explode('.', $key) as $k) {
if (!is_array($return) || !isset($return[$k])) {
$return = null;
break;
}
$return = $return[$k];
}
return $return;
}
/**
* Writes a config key.
*
* @param array<string, mixed>|string $key Key to write to.
* @param mixed $value Value to write.
* @param string|bool $merge True to merge recursively, 'shallow' for simple merge,
* false to overwrite, defaults to false.
* @return void
* @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config
*/
protected function _configWrite(array|string $key, mixed $value, string|bool $merge = false): void
{
if (is_string($key) && $value === null) {
$this->_configDelete($key);
return;
}
if ($merge) {
$update = is_array($key) ? $key : [$key => $value];
if ($merge === 'shallow') {
$this->_config = array_merge($this->_config, Hash::expand($update));
} else {
$this->_config = Hash::merge($this->_config, Hash::expand($update));
}
return;
}
if (is_array($key)) {
foreach ($key as $k => $val) {
$this->_configWrite($k, $val);
}
return;
}
if (!str_contains($key, '.')) {
$this->_config[$key] = $value;
return;
}
$update = &$this->_config;
$stack = explode('.', $key);
foreach ($stack as $k) {
if (!is_array($update)) {
throw new CakeException(sprintf('Cannot set `%s` value.', $key));
}
$update[$k] ??= [];
$update = &$update[$k];
}
$update = $value;
}
/**
* Deletes a single config key.
*
* @param string $key Key to delete.
* @return void
* @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config
*/
protected function _configDelete(string $key): void
{
if (!str_contains($key, '.')) {
unset($this->_config[$key]);
return;
}
$update = &$this->_config;
$stack = explode('.', $key);
$length = count($stack);
foreach ($stack as $i => $k) {
if (!is_array($update)) {
throw new CakeException(sprintf('Cannot unset `%s` value.', $key));
}
if (!isset($update[$k])) {
break;
}
if ($i === $length - 1) {
unset($update[$k]);
break;
}
$update = &$update[$k];
}
}
}
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org)
Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org)
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.
+406
View File
@@ -0,0 +1,406 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use ArrayIterator;
use Cake\Core\Exception\CakeException;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventListenerInterface;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* Acts as a registry/factory for objects.
*
* Provides registry & factory functionality for object types. Used
* as a super class for various composition based re-use features in CakePHP.
*
* Each subclass needs to implement the various abstract methods to complete
* the template method load().
*
* The ObjectRegistry is EventManager aware, but each extending class will need to use
* \Cake\Event\EventDispatcherTrait to attach and detach on set and bind
*
* @see \Cake\Controller\ComponentRegistry
* @see \Cake\View\HelperRegistry
* @see \Cake\Console\TaskRegistry
* @template TObject of object
* @template-implements \IteratorAggregate<string, TObject>
*/
abstract class ObjectRegistry implements Countable, IteratorAggregate
{
/**
* Map of loaded objects.
*
* @var array<string, TObject>
*/
protected array $_loaded = [];
/**
* Loads/constructs an object instance.
*
* Will return the instance in the registry if it already exists.
* If a subclass provides event support, you can use `$config['enabled'] = false`
* to exclude constructed objects from being registered for events.
*
* Using {@link \Cake\Controller\Component::$components} as an example. You can alias
* an object by setting the 'className' key, i.e.,
*
* ```
* protected $components = [
* 'Email' => [
* 'className' => 'App\Controller\Component\AliasedEmailComponent'
* ];
* ];
* ```
*
* All calls to the `Email` component would use `AliasedEmail` instead.
*
* @param string $name The name/class of the object to load.
* @param array<string, mixed> $config Additional settings to use when loading the object.
* @return TObject
* @throws \Exception If the class cannot be found.
*/
public function load(string $name, array $config = []): object
{
if (isset($config['className'])) {
if ($name === $config['className']) {
[, $objName] = pluginSplit($name);
} else {
$objName = $name;
}
$name = $config['className'];
} else {
[$plugin, $objName] = pluginSplit($name);
if ($plugin) {
$config['className'] = $name;
}
}
$loaded = isset($this->_loaded[$objName]);
if ($loaded && $config !== []) {
$this->_checkDuplicate($objName, $config);
}
if ($loaded) {
return $this->_loaded[$objName];
}
$className = $name;
if (is_string($name)) {
$className = $this->_resolveClassName($name);
if ($className === null) {
[$plugin, $name] = pluginSplit($name);
$this->_throwMissingClassError($name, $plugin);
}
}
$instance = $this->_create($className, $objName, $config);
$this->_loaded[$objName] = $instance;
return $instance;
}
/**
* Check for duplicate object loading.
*
* If a duplicate is being loaded and has different configuration, that is
* bad and an exception will be raised.
*
* An exception is raised, as replacing the object will not update any
* references other objects may have. Additionally, simply updating the runtime
* configuration is not a good option as we may be missing important constructor
* logic dependent on the configuration.
*
* @param string $name The name of the alias in the registry.
* @param array<string, mixed> $config The config data for the new instance.
* @return void
* @throws \Cake\Core\Exception\CakeException When a duplicate is found.
*/
protected function _checkDuplicate(string $name, array $config): void
{
$existing = $this->_loaded[$name];
$msg = sprintf('The `%s` alias has already been loaded.', $name);
$hasConfig = method_exists($existing, 'getConfig');
if (!$hasConfig) {
throw new CakeException($msg);
}
if (!$config) {
return;
}
$existingConfig = $existing->getConfig();
unset($config['enabled'], $existingConfig['enabled']);
$failure = null;
foreach ($config as $key => $value) {
if (!array_key_exists($key, $existingConfig)) {
$failure = " The `{$key}` was not defined in the previous configuration data.";
break;
}
if (isset($existingConfig[$key]) && $existingConfig[$key] !== $value) {
$failure = sprintf(
' The `%s` key has a value of `%s` but previously had a value of `%s`',
$key,
json_encode($value, JSON_THROW_ON_ERROR),
json_encode($existingConfig[$key], JSON_THROW_ON_ERROR),
);
break;
}
}
if ($failure) {
throw new CakeException($msg . $failure);
}
}
/**
* Should resolve the classname for a given object type.
*
* @param string $class The class to resolve.
* @return class-string<TObject>|null The resolved name or null for failure.
*/
abstract protected function _resolveClassName(string $class): ?string;
/**
* Throw an exception when the requested object name is missing.
*
* @param string $class The class that is missing.
* @param string|null $plugin The plugin $class is missing from.
* @return void
* @throws \Exception
*/
abstract protected function _throwMissingClassError(string $class, ?string $plugin): void;
/**
* Create an instance of a given classname.
*
* This method should construct and do any other initialization logic
* required.
*
* @param TObject|class-string<TObject> $class The class to build.
* @param string $alias The alias of the object.
* @param array<string, mixed> $config The Configuration settings for construction
* @return TObject
*/
abstract protected function _create(object|string $class, string $alias, array $config): object;
/**
* Get the list of loaded objects.
*
* @return array<string> List of object names.
*/
public function loaded(): array
{
return array_keys($this->_loaded);
}
/**
* Check whether a given object is loaded.
*
* @param string $name The object name to check for.
* @return bool True is object is loaded else false.
*/
public function has(string $name): bool
{
return isset($this->_loaded[$name]);
}
/**
* Get loaded object instance.
*
* @param string $name Name of object.
* @return TObject Object instance.
* @throws \Cake\Core\Exception\CakeException If not loaded or found.
*/
public function get(string $name): object
{
if (!isset($this->_loaded[$name])) {
throw new CakeException(sprintf('Unknown object `%s`.', $name));
}
return $this->_loaded[$name];
}
/**
* Provide public read access to the loaded objects
*
* @param string $name Name of property to read
* @return TObject|null
*/
public function __get(string $name): ?object
{
return $this->_loaded[$name] ?? null;
}
/**
* Provide isset access to _loaded
*
* @param string $name Name of object being checked.
* @return bool
*/
public function __isset(string $name): bool
{
return $this->has($name);
}
/**
* Sets an object.
*
* @param string $name Name of a property to set.
* @param TObject $object Object to set.
* @return void
*/
public function __set(string $name, object $object): void
{
$this->set($name, $object);
}
/**
* Unsets an object.
*
* @param string $name Name of a property to unset.
* @return void
*/
public function __unset(string $name): void
{
$this->unload($name);
}
/**
* Normalizes an object configuration array into associative form for making
* lazy loading easier.
*
* @param array $objects Array of child objects to normalize.
* @return array<string, array> Array of normalized objects.
*/
public function normalizeArray(array $objects): array
{
$normal = [];
foreach ($objects as $objectName => $config) {
if (is_int($objectName)) {
$objectName = $config;
$config = [];
}
[$plugin, $name] = pluginSplit($objectName);
if ($plugin) {
$config['className'] = $objectName;
}
$normal[$name] = $config;
}
return $normal;
}
/**
* Clear loaded instances in the registry.
*
* If the registry subclass has an event manager, the objects will be detached from events as well.
*
* @return $this
*/
public function reset()
{
foreach (array_keys($this->_loaded) as $name) {
$this->unload((string)$name);
}
return $this;
}
/**
* Set an object directly into the registry by name.
*
* If this collection implements events, the passed object will
* be attached into the event manager
*
* @param string $name The name of the object to set in the registry.
* @param TObject $object instance to store in the registry
* @return $this
*/
public function set(string $name, object $object)
{
// Just call unload if the object was loaded before
if (array_key_exists($name, $this->_loaded)) {
$this->unload($name);
}
if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
$this->getEventManager()->on($object);
}
$this->_loaded[$name] = $object;
return $this;
}
/**
* Remove an object from the registry.
*
* If this registry has an event manager, the object will be detached from any events as well.
*
* @param string $name The name of the object to remove from the registry.
* @return $this
*/
public function unload(string $name)
{
if (!isset($this->_loaded[$name])) {
throw new CakeException(sprintf('Object named `%s` is not loaded.', $name));
}
$object = $this->_loaded[$name];
if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
$this->getEventManager()->off($object);
}
unset($this->_loaded[$name]);
return $this;
}
/**
* Returns an array iterator.
*
* @return \Traversable<string, TObject>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->_loaded);
}
/**
* Returns the number of loaded objects.
*
* @return int
*/
public function count(): int
{
return count($this->_loaded);
}
/**
* Debug friendly object properties.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$properties = get_object_vars($this);
if (isset($properties['_loaded'])) {
$properties['_loaded'] = array_keys($properties['_loaded']);
}
return $properties;
}
}
+143
View File
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Plugin is used to load and locate plugins.
*
* It also can retrieve plugin paths and load their bootstrap and routes files.
*
* @link https://book.cakephp.org/5/en/plugins.html
*/
class Plugin
{
/**
* Holds a list of all loaded plugins and their configuration
*
* @var \Cake\Core\PluginCollection|null
*/
protected static ?PluginCollection $plugins = null;
/**
* Returns the filesystem path for a plugin
*
* @param string $name name of the plugin in CamelCase format
* @return string path to the plugin folder
* @throws \Cake\Core\Exception\MissingPluginException If the folder for plugin was not found
* or plugin has not been loaded.
*/
public static function path(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getPath();
}
/**
* Returns the filesystem path for plugin's folder containing class files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing class files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function classPath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getClassPath();
}
/**
* Returns the filesystem path for plugin's folder containing config files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing config files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function configPath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getConfigPath();
}
/**
* Returns the filesystem path for plugin's folder containing template files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing template files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function templatePath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getTemplatePath();
}
/**
* Returns true if the plugin $plugin is already loaded.
*
* @param string $plugin Plugin name.
* @return bool
* @since 3.7.0
*/
public static function isLoaded(string $plugin): bool
{
return static::getCollection()->has($plugin);
}
/**
* Return a list of loaded plugins.
*
* @return array<string> A list of plugins that have been loaded
*/
public static function loaded(): array
{
$names = [];
foreach (static::getCollection() as $plugin) {
$names[] = $plugin->getName();
}
sort($names);
return $names;
}
/**
* Get the shared plugin collection.
*
* This method should generally not be used during application
* runtime as plugins should be set during Application startup.
*
* @return \Cake\Core\PluginCollection
*/
public static function getCollection(): PluginCollection
{
return static::$plugins ??= new PluginCollection();
}
/**
* Set the shared plugin collection.
*
* @param \Cake\Core\PluginCollection $collection
* @return void
*/
public static function setCollection(PluginCollection $collection): void
{
static::$plugins = $collection;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Event\EventDispatcherInterface;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
/**
* Interface for Applications that leverage plugins & events.
*
* Events can be bound to the application event manager during
* the application's bootstrap and plugin bootstrap.
*
* @template TSubject
* @extends \Cake\Event\EventDispatcherInterface<\Cake\Http\BaseApplication>
*/
interface PluginApplicationInterface extends EventDispatcherInterface
{
/**
* Add a plugin to the loaded plugin set.
*
* If the named plugin does not exist, or does not define a Plugin class, an
* instance of `Cake\Core\BasePlugin` will be used. This generated class will have
* all plugin hooks enabled.
*
* @param \Cake\Core\PluginInterface|string $name The plugin name or plugin object.
* @param array<string, mixed> $config The configuration data for the plugin if using a string for $name
* @return $this
*/
public function addPlugin(PluginInterface|string $name, array $config = []);
/**
* Run bootstrap logic for loaded plugins.
*
* @return void
*/
public function pluginBootstrap(): void;
/**
* Run routes hooks for loaded plugins
*
* @param \Cake\Routing\RouteBuilder $routes The route builder to use.
* @return \Cake\Routing\RouteBuilder
*/
public function pluginRoutes(RouteBuilder $routes): RouteBuilder;
/**
* Run middleware hooks for plugins
*
* @param \Cake\Http\MiddlewareQueue $middleware The MiddlewareQueue to use.
* @return \Cake\Http\MiddlewareQueue
*/
public function pluginMiddleware(MiddlewareQueue $middleware): MiddlewareQueue;
/**
* Run console hooks for plugins
*
* @param \Cake\Console\CommandCollection $commands The CommandCollection to use.
* @return \Cake\Console\CommandCollection
*/
public function pluginConsole(CommandCollection $commands): CommandCollection;
}
+400
View File
@@ -0,0 +1,400 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\MissingPluginException;
use Cake\Utility\Hash;
use Countable;
use Generator;
use InvalidArgumentException;
use Iterator;
/**
* Plugin Collection
*
* Holds onto plugin objects loaded into an application, and
* provides methods for iterating, and finding plugins based
* on criteria.
*
* This class implements the Iterator interface to allow plugins
* to be iterated, handling the situation where a plugin's hook
* method (usually bootstrap) loads another plugin during iteration.
*
* While its implementation supported nested iteration it does not
* support using `continue` or `break` inside loops.
*
* @template-implements \Iterator<string, \Cake\Core\PluginInterface>
*/
class PluginCollection implements Iterator, Countable
{
/**
* Plugin list
*
* @var array<string, \Cake\Core\PluginInterface>
*/
protected array $plugins = [];
/**
* Names of plugins
*
* @var array<string>
*/
protected array $names = [];
/**
* Iterator position stack.
*
* @var array<int>
*/
protected array $positions = [];
/**
* Loop depth
*
* @var int
*/
protected int $loopDepth = -1;
/**
* Constructor
*
* @param array<\Cake\Core\PluginInterface> $plugins The map of plugins to add to the collection.
*/
public function __construct(array $plugins = [])
{
foreach ($plugins as $plugin) {
$this->add($plugin);
}
PluginConfig::loadInstallerConfig();
}
/**
* Add plugins from config array.
*
* @param array $config Configuration array. For e.g.:
* ```
* [
* 'Company/TestPluginThree',
* 'TestPlugin' => ['onlyDebug' => true, 'onlyCli' => true],
* 'Nope' => ['optional' => true],
* 'Named' => ['routes' => false, 'bootstrap' => false],
* ]
* ```
* @return void
*/
public function addFromConfig(array $config): void
{
$notDebug = !Configure::read('debug');
$notCli = PHP_SAPI !== 'cli';
/** @var array{onlyDebug?: bool, onlyCli?: bool, optional?: bool} $options */
foreach (Hash::normalize($config, default: []) as $name => $options) {
$onlyDebug = $options['onlyDebug'] ?? false;
$onlyCli = $options['onlyCli'] ?? false;
$optional = $options['optional'] ?? false;
if (
($onlyDebug && $notDebug)
|| ($onlyCli && $notCli)
) {
continue;
}
try {
$plugin = $this->create($name, $options);
$this->add($plugin);
} catch (MissingPluginException $e) {
if (!$optional) {
throw $e;
}
}
}
}
/**
* Locate a plugin path by looking at configuration data.
*
* This will use the `plugins` Configure key, and fallback to enumerating `App::path('plugins')`
*
* This method is not part of the official public API as plugins with
* no plugin class are being phased out.
*
* @param string $name The plugin name to locate a path for.
* @return string
* @throws \Cake\Core\Exception\MissingPluginException when a plugin path cannot be resolved.
* @internal
*/
public function findPath(string $name): string
{
// Ensure plugin config is loaded each time. This is necessary primarily
// for testing because the Configure::clear() call in TestCase::tearDown()
// wipes out all configuration including plugin paths config.
PluginConfig::loadInstallerConfig();
/** @var string|null $path */
$path = Configure::read('plugins.' . $name);
if ($path) {
return $path;
}
$pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $name);
$paths = App::path('plugins');
foreach ($paths as $path) {
if (is_dir($path . $pluginPath)) {
return $path . $pluginPath . DIRECTORY_SEPARATOR;
}
}
throw new MissingPluginException(['plugin' => $name]);
}
/**
* Add a plugin to the collection
*
* Plugins will be keyed by their names.
*
* @param \Cake\Core\PluginInterface $plugin The plugin to load.
* @return $this
*/
public function add(PluginInterface $plugin)
{
$name = $plugin->getName();
if (isset($this->plugins[$name])) {
throw new CakeException(sprintf('Plugin named `%s` is already loaded', $name));
}
$this->plugins[$name] = $plugin;
$this->names = array_keys($this->plugins);
return $this;
}
/**
* Remove a plugin from the collection if it exists.
*
* @param string $name The named plugin.
* @return $this
*/
public function remove(string $name)
{
unset($this->plugins[$name]);
$this->names = array_keys($this->plugins);
return $this;
}
/**
* Remove all plugins from the collection
*
* @return $this
*/
public function clear()
{
$this->plugins = [];
$this->names = [];
$this->positions = [];
$this->loopDepth = -1;
return $this;
}
/**
* Check whether the named plugin exists in the collection.
*
* @param string $name The named plugin.
* @return bool
*/
public function has(string $name): bool
{
return isset($this->plugins[$name]);
}
/**
* Get the a plugin by name.
*
* If a plugin isn't already loaded it will be autoloaded on first access
* and that plugins loaded this way may miss some hook methods.
*
* @param string $name The plugin to get.
* @return \Cake\Core\PluginInterface The plugin.
* @throws \Cake\Core\Exception\MissingPluginException when unknown plugins are fetched.
*/
public function get(string $name): PluginInterface
{
if ($this->has($name)) {
return $this->plugins[$name];
}
$plugin = $this->create($name);
$this->add($plugin);
return $plugin;
}
/**
* Create a plugin instance from a name/classname and configuration.
*
* @param string $name The plugin name or classname
* @param array<string, mixed> $config Configuration options for the plugin.
* @return \Cake\Core\PluginInterface
* @throws \Cake\Core\Exception\MissingPluginException When plugin instance could not be created.
* @throws \InvalidArgumentException When class name cannot be found or an empty name is provided.
* @phpstan-param class-string<\Cake\Core\PluginInterface>|string $name
*/
public function create(string $name, array $config = []): PluginInterface
{
if ($name === '') {
throw new InvalidArgumentException('Plugin name cannot be empty.');
}
if (str_contains($name, '\\')) {
if (!class_exists($name)) {
throw new InvalidArgumentException(sprintf('Class `%s` does not exist.', $name));
}
/** @var \Cake\Core\PluginInterface */
return new $name($config);
}
$config += ['name' => $name];
$namespace = str_replace('/', '\\', $name);
$pos = strpos($name, '/');
$namePart = $pos === false ? $name : substr($name, $pos + 1);
// Check for [Vendor/]Foo/FooPlugin class
$className = $namespace . '\\' . $namePart . 'Plugin';
if (!class_exists($className)) {
// Check for [Vendor/]Foo/Plugin class
$className = $namespace . '\\' . 'Plugin';
if (class_exists($className)) {
deprecationWarning(
'5.3.0',
'Loading plugins with a plugin class named `Plugin` is deprecated.'
. " Rename the class to `{$namePart}Plugin` instead.",
);
} else {
$className = BasePlugin::class;
if (empty($config['path'])) {
$config['path'] = $this->findPath($name);
}
deprecationWarning(
'5.3.0',
'Loading plugins without a plugin class is deprecated.'
. " You can create the missing class using `bin/cake bake plugin {$name} --class-only`.",
);
}
}
/** @var class-string<\Cake\Core\PluginInterface> $className */
return new $className($config);
}
/**
* Implementation of Countable.
*
* Get the number of plugins in the collection.
*
* @return int
*/
public function count(): int
{
return count($this->plugins);
}
/**
* Part of Iterator Interface
*
* @return void
*/
public function next(): void
{
$this->positions[$this->loopDepth]++;
}
/**
* Part of Iterator Interface
*
* @return string
*/
public function key(): string
{
return $this->names[$this->positions[$this->loopDepth]];
}
/**
* Part of Iterator Interface
*
* @return \Cake\Core\PluginInterface
*/
public function current(): PluginInterface
{
$position = $this->positions[$this->loopDepth];
$name = $this->names[$position];
return $this->plugins[$name];
}
/**
* Part of Iterator Interface
*
* @return void
*/
public function rewind(): void
{
$this->positions[] = 0;
$this->loopDepth += 1;
}
/**
* Part of Iterator Interface
*
* @return bool
*/
public function valid(): bool
{
$valid = isset($this->names[$this->positions[$this->loopDepth]]);
if (!$valid) {
array_pop($this->positions);
$this->loopDepth -= 1;
}
return $valid;
}
/**
* Filter the plugins to those with the named hook enabled.
*
* @param string $hook The hook to filter plugins by
* @return \Generator<\Cake\Core\PluginInterface> A generator containing matching plugins.
* @throws \InvalidArgumentException on invalid hooks
*/
public function with(string $hook): Generator
{
if (!in_array($hook, PluginInterface::VALID_HOOKS, true)) {
throw new InvalidArgumentException(sprintf('The `%s` hook is not a known plugin hook.', $hook));
}
foreach ($this as $plugin) {
if ($plugin->isEnabled($hook)) {
yield $plugin;
}
}
}
}
+187
View File
@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
/**
* PluginConfig contains all available plugins and their config if/how they should be loaded
*
* @internal
*/
class PluginConfig
{
/**
* Load the path information stored in vendor/cakephp-plugins.php
*
* This file is generated by the cakephp/plugin-installer package and used
* to locate plugins on the filesystem as applications can use `extra.plugin-paths`
* in their composer.json file to move plugin outside of vendor/
*
* @internal
* @return void
*/
public static function loadInstallerConfig(): void
{
if (Configure::check('plugins')) {
return;
}
$vendorFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
if (!is_file($vendorFile)) {
$vendorFile = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
if (!is_file($vendorFile)) {
Configure::write(['plugins' => []]);
return;
}
}
$config = require $vendorFile;
Configure::write($config);
}
/**
* Get the config how plugins should be loaded
*
* @param string|null $path The absolute path to the composer.lock file to retrieve the versions from
* @return array
*/
public static function getAppConfig(?string $path = null): array
{
self::loadInstallerConfig();
// phpcs:ignore
$pluginLoadConfig = @include CONFIG . 'plugins.php';
if (is_array($pluginLoadConfig)) {
$pluginLoadConfig = Hash::normalize($pluginLoadConfig);
} else {
$pluginLoadConfig = [];
}
try {
$composerVersions = self::getVersions($path);
} catch (CakeException) {
$composerVersions = [];
}
$result = [];
$availablePlugins = Configure::read('plugins', []);
if ($availablePlugins && is_array($availablePlugins)) {
foreach ($availablePlugins as $pluginName => $pluginPath) {
if ($pluginLoadConfig && array_key_exists($pluginName, $pluginLoadConfig)) {
$options = $pluginLoadConfig[$pluginName];
$hooks = PluginInterface::VALID_HOOKS;
$mainConfig = [
'isLoaded' => true,
'onlyDebug' => $options['onlyDebug'] ?? false,
'onlyCli' => $options['onlyCli'] ?? false,
'optional' => $options['optional'] ?? false,
];
foreach ($hooks as $hook) {
$mainConfig[$hook] = $options[$hook] ?? true;
}
$result[$pluginName] = $mainConfig;
} else {
$result[$pluginName]['isLoaded'] = false;
}
try {
$packageName = self::getPackageNameFromPath($pluginPath);
$result[$pluginName]['packagePath'] = $pluginPath;
$result[$pluginName]['package'] = $packageName;
} catch (CakeException) {
$packageName = null;
}
if ($composerVersions && $packageName) {
if (array_key_exists($packageName, $composerVersions['packages'])) {
$result[$pluginName]['version'] = $composerVersions['packages'][$packageName];
$result[$pluginName]['isDevPackage'] = false;
} elseif (array_key_exists($packageName, $composerVersions['devPackages'])) {
$result[$pluginName]['version'] = $composerVersions['devPackages'][$packageName];
$result[$pluginName]['isDevPackage'] = true;
}
}
}
}
$diff = array_diff(array_keys($pluginLoadConfig), array_keys($availablePlugins));
foreach ($diff as $unknownPlugin) {
$result[$unknownPlugin]['isLoaded'] = false;
$result[$unknownPlugin]['isUnknown'] = true;
}
return $result;
}
/**
* @param string|null $path The absolute path to the composer.lock file to retrieve the versions from
* @return array
*/
public static function getVersions(?string $path = null): array
{
$lockFilePath = $path ?? ROOT . DIRECTORY_SEPARATOR . 'composer.lock';
if (!file_exists($lockFilePath)) {
throw new CakeException(sprintf('composer.lock does not exist in %s', $lockFilePath));
}
$lockFile = file_get_contents($lockFilePath);
if ($lockFile === false) {
throw new CakeException(sprintf('Could not read composer.lock: %s', $lockFilePath));
}
$lockFileJson = json_decode($lockFile, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing composer.lock: %s',
json_last_error_msg(),
));
}
$packages = Hash::combine($lockFileJson['packages'], '{n}.name', '{n}.version');
$devPackages = Hash::combine($lockFileJson['packages-dev'], '{n}.name', '{n}.version');
return [
'packages' => $packages,
'devPackages' => $devPackages,
];
}
/**
* @param string $path
* @return string
*/
protected static function getPackageNameFromPath(string $path): string
{
$jsonPath = $path . DS . 'composer.json';
if (!file_exists($jsonPath)) {
throw new CakeException(sprintf('composer.json does not exist in %s', $jsonPath));
}
$jsonString = file_get_contents($jsonPath);
if ($jsonString === false) {
throw new CakeException(sprintf('Could not read composer.json: %s', $jsonPath));
}
$json = json_decode($jsonString, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing %ss: %s',
$jsonPath,
json_last_error_msg(),
));
}
return $json['name'];
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
/**
* Plugin Interface
*
* @method \Cake\Event\EventManagerInterface events(\Cake\Event\EventManagerInterface $eventManager)
*/
interface PluginInterface
{
/**
* List of valid hooks.
*
* @var array<string>
*/
public const VALID_HOOKS = ['bootstrap', 'console', 'middleware', 'routes', 'services', 'events'];
/**
* Get the name of this plugin.
*
* @return string
*/
public function getName(): string;
/**
* Get the filesystem path to this plugin
*
* @return string
*/
public function getPath(): string;
/**
* Get the filesystem path to configuration for this plugin
*
* @return string
*/
public function getConfigPath(): string;
/**
* Get the filesystem path to configuration for this plugin
*
* @return string
*/
public function getClassPath(): string;
/**
* Get the filesystem path to templates for this plugin
*
* @return string
*/
public function getTemplatePath(): string;
/**
* Load all the application configuration and bootstrap logic.
*
* The default implementation of this method will include the `config/bootstrap.php` in the plugin if it exist. You
* can override this method to replace that behavior.
*
* The host application is provided as an argument. This allows you to load additional
* plugin dependencies, or attach events.
*
* @param \Cake\Core\PluginApplicationInterface $app The host application
* @return void
*/
public function bootstrap(PluginApplicationInterface $app): void;
/**
* Add console commands for the plugin.
*
* @param \Cake\Console\CommandCollection $commands The command collection to update
* @return \Cake\Console\CommandCollection
*/
public function console(CommandCollection $commands): CommandCollection;
/**
* Add middleware for the plugin.
*
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
* @return \Cake\Http\MiddlewareQueue
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
/**
* Add routes for the plugin.
*
* The default implementation of this method will include the `config/routes.php` in the plugin if it exists. You
* can override this method to replace that behavior.
*
* @param \Cake\Routing\RouteBuilder $routes The route builder to update.
* @return void
*/
public function routes(RouteBuilder $routes): void;
/**
* Register plugin services to the application's container
*
* @param \Cake\Core\ContainerInterface $container Container instance.
* @return void
*/
public function services(ContainerInterface $container): void;
/**
* Disables the named hook
*
* @param string $hook The hook to disable
* @return $this
*/
public function disable(string $hook);
/**
* Enables the named hook
*
* @param string $hook The hook to disable
* @return $this
*/
public function enable(string $hook);
/**
* Check if the named hook is enabled
*
* @param string $hook The hook to check
* @return bool
*/
public function isEnabled(string $hook): bool;
}
+37
View File
@@ -0,0 +1,37 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/core.svg?style=flat-square)](https://packagist.org/packages/cakephp/core)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt)
# CakePHP Core Classes
A set of classes used for configuration files reading and storing.
This repository contains the classes that are used as glue for creating the CakePHP framework.
## Usage
You can use the `Configure` class to store arbitrary configuration data:
```php
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
Configure::write('Company.name','Pizza, Inc.');
Configure::read('Company.name'); // Returns: 'Pizza, Inc.'
```
It also possible to load configuration from external files:
```php
Configure::config('default', new PhpConfig('/path/to/config/folder'));
Configure::load('app', 'default', false);
Configure::load('other_config', 'default');
```
And write the configuration back into files:
```php
Configure::dump('my_config', 'default');
```
## Documentation
Please make sure you check the [official documentation](https://book.cakephp.org/5/en/development/configuration.html)
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Retry;
use Closure;
use Exception;
/**
* Allows any action to be retried in case of an exception.
*
* This class can be parametrized with a strategy, which will be followed
* to determine whether the action should be retried.
*/
class CommandRetry
{
/**
* The strategy to follow should the executed action fail.
*
* @var \Cake\Core\Retry\RetryStrategyInterface
*/
protected RetryStrategyInterface $strategy;
/**
* @var int
*/
protected int $maxRetries;
/**
* @var int
*/
protected int $numRetries;
/**
* Creates the CommandRetry object with the given strategy and retry count
*
* @param \Cake\Core\Retry\RetryStrategyInterface $strategy The strategy to follow should the action fail
* @param int $maxRetries The maximum number of retry attempts allowed
*/
public function __construct(RetryStrategyInterface $strategy, int $maxRetries = 1)
{
$this->strategy = $strategy;
$this->maxRetries = $maxRetries;
}
/**
* The number of retries to perform in case of failure
*
* @param \Closure $action Callback to run for each attempt
* @return mixed The return value of the passed action callable
* @throws \Exception Throws exception from last failure
*/
public function run(Closure $action): mixed
{
$this->numRetries = 0;
while (true) {
try {
return $action();
} catch (Exception $e) {
if (
$this->numRetries < $this->maxRetries &&
$this->strategy->shouldRetry($e, $this->numRetries)
) {
$this->numRetries++;
continue;
}
throw $e;
}
}
}
/**
* Returns the last number of retry attempts.
*
* @return int
*/
public function getRetries(): int
{
return $this->numRetries;
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Retry;
use Exception;
/**
* Used to instruct a CommandRetry object on whether a retry
* for an action should be performed
*/
interface RetryStrategyInterface
{
/**
* Returns true if the action can be retried, false otherwise.
*
* @param \Exception $exception The exception that caused the action to fail
* @param int $retryCount The number of times action has been retried
* @return bool Whether it is OK to retry the action
*/
public function shouldRetry(Exception $exception, int $retryCount): bool;
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Read-only wrapper for configuration data
*
* Intended for use with {@link \Cake\Core\Container} as
* a typehintable way for services to have application
* configuration injected as arrays cannot be typehinted.
*/
class ServiceConfig
{
/**
* Read a configuration key
*
* @param string $path The path to read.
* @param mixed $default The default value to use if $path does not exist.
* @return mixed The configuration data or $default value.
*/
public function get(string $path, mixed $default = null): mixed
{
return Configure::read($path, $default);
}
/**
* Check if $path exists and has a non-null value.
*
* @param string $path The path to check.
* @return bool True if the configuration data exists.
*/
public function has(string $path): bool
{
return Configure::check($path);
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\DefinitionContainerInterface;
use League\Container\ServiceProvider\AbstractServiceProvider;
use League\Container\ServiceProvider\BootableServiceProviderInterface;
use LogicException;
/**
* Container ServiceProvider
*
* Service provider bundle related services together helping
* to organize your application's dependencies. They also help
* improve performance of applications with many services by
* allowing service registration to be deferred until services are needed.
*/
abstract class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface
{
/**
* List of ids of services this provider provides.
*
* @var array<string>
* @see ServiceProvider::provides()
*/
protected array $provides = [];
/**
* Get the container.
*
* @return \Cake\Core\ContainerInterface
*/
public function getContainer(): DefinitionContainerInterface
{
$container = parent::getContainer();
assert(
$container instanceof ContainerInterface,
sprintf(
'Unexpected container type. Expected `%s` got `%s` instead.',
ContainerInterface::class,
get_debug_type($container),
),
);
return $container;
}
/**
* Delegate to the bootstrap() method
*
* This method wraps the league/container function so users
* only need to use the CakePHP bootstrap() interface.
*
* @return void
*/
public function boot(): void
{
$this->bootstrap($this->getContainer());
}
/**
* Bootstrap hook for ServiceProviders
*
* This hook should be implemented if your service provider
* needs to register additional service providers, load configuration
* files or do any other work when the service provider is added to the
* container.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
public function bootstrap(ContainerInterface $container): void
{
}
/**
* Call the abstract services() method.
*
* This method primarily exists as a shim between the interface
* that league/container has and the one we want to offer in CakePHP.
*
* @return void
*/
public function register(): void
{
$this->services($this->getContainer());
}
/**
* The provides method is a way to let the container know that a service
* is provided by this service provider.
*
* Every service registered via this service provider must have an
* alias added to this array or it will be ignored.
*
* @param string $id Identifier.
* @return bool
*/
public function provides(string $id): bool
{
if (!$this->provides) {
throw new LogicException(
'The property `$provides` should contain a list with service ids for this service provider',
);
}
return in_array($id, $this->provides, true);
}
/**
* Register the services in a provider.
*
* All services registered in this method should also be included in the $provides
* property so that services can be located.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
abstract public function services(ContainerInterface $container): void;
}
+333
View File
@@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use BadMethodCallException;
use InvalidArgumentException;
use LogicException;
/**
* A trait that provides a set of static methods to manage configuration
* for classes that provide an adapter facade or need to have sets of
* configuration data registered and manipulated.
*
* Implementing objects are expected to declare a static `$_dsnClassMap` property.
*/
trait StaticConfigTrait
{
/**
* Configuration sets.
*
* @var array<string|int, array<string, mixed>>
*/
protected static array $_config = [];
/**
* This method can be used to define configuration adapters for an application.
*
* To change an adapter's configuration at runtime, first drop the adapter and then
* reconfigure it.
*
* Adapters will not be constructed until the first operation is done.
*
* ### Usage
*
* Assuming that the class' name is `Cache` the following scenarios
* are supported:
*
* Setting a cache engine up.
*
* ```
* Cache::setConfig('default', $settings);
* ```
*
* Injecting a constructed adapter in:
*
* ```
* Cache::setConfig('default', $instance);
* ```
*
* Configure multiple adapters at once:
*
* ```
* Cache::setConfig($arrayOfConfig);
* ```
*
* @param array<string, mixed>|string $key The name of the configuration, or an array of multiple configs.
* @param mixed $config Configuration value. Generally an array of name => configuration data for adapter.
* @throws \BadMethodCallException When trying to modify an existing config.
* @throws \LogicException When trying to store an invalid structured config array.
* @return void
*/
public static function setConfig(array|string $key, mixed $config = null): void
{
if ($config === null) {
if (!is_array($key)) {
throw new LogicException('If config is null, key must be an array.');
}
foreach ($key as $name => $settings) {
static::setConfig((string)$name, $settings);
}
return;
}
if (!is_string($key)) {
throw new LogicException('If config is not null, key must be a string.');
}
if (isset(static::$_config[$key])) {
throw new BadMethodCallException(sprintf('Cannot reconfigure existing key `%s`.', $key));
}
if (is_object($config)) {
$config = ['className' => $config];
}
if (is_array($config) && isset($config['url'])) {
$parsed = static::parseDsn($config['url']);
unset($config['url']);
$config = $parsed + $config;
}
if (isset($config['engine']) && empty($config['className'])) {
$config['className'] = $config['engine'];
unset($config['engine']);
}
static::$_config[$key] = $config;
}
/**
* Reads existing configuration.
*
* @param string $key The name of the configuration.
* @return mixed|null Configuration data at the named key or null if the key does not exist.
*/
public static function getConfig(string $key): mixed
{
return static::$_config[$key] ?? null;
}
/**
* Reads existing configuration for a specific key.
*
* The config value for this key must exist, it can never be null.
*
* @param string $key The name of the configuration.
* @return mixed Configuration data at the named key.
* @throws \InvalidArgumentException If value does not exist.
*/
public static function getConfigOrFail(string $key): mixed
{
if (!isset(static::$_config[$key])) {
throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
}
return static::$_config[$key];
}
/**
* Drops a constructed adapter.
*
* If you wish to modify an existing configuration, you should drop it,
* change configuration and then re-add it.
*
* If the implementing objects supports a `$_registry` object the named configuration
* will also be unloaded from the registry.
*
* @param string $config An existing configuration you wish to remove.
* @return bool Success of the removal, returns false when the config does not exist.
*/
public static function drop(string $config): bool
{
if (!isset(static::$_config[$config])) {
return false;
}
/** @phpstan-ignore-next-line */
if (isset(static::$_registry)) {
static::$_registry->unload($config);
}
unset(static::$_config[$config]);
return true;
}
/**
* Returns an array containing the named configurations
*
* @return array<string> Array of configurations.
*/
public static function configured(): array
{
$configurations = array_keys(static::$_config);
return array_map(function (int|string $key) {
return (string)$key;
}, $configurations);
}
/**
* Parses a DSN into a valid connection configuration
*
* This method allows setting a DSN using formatting similar to that used by PEAR::DB.
* The following is an example of its usage:
*
* ```
* $dsn = 'mysql://user:pass@localhost/database?';
* $config = ConnectionManager::parseDsn($dsn);
*
* $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS';
* $config = Log::parseDsn($dsn);
*
* $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null';
* $config = Email::parseDsn($dsn);
*
* $dsn = 'file:///?className=\My\Cache\Engine\FileEngine';
* $config = Cache::parseDsn($dsn);
*
* $dsn = 'File://?prefix=myapp_cake_translations_&serialize=true&duration=+2 minutes&path=/tmp/persistent/';
* $config = Cache::parseDsn($dsn);
* ```
*
* For all classes, the value of `scheme` is set as the value of both the `className`
* unless they have been otherwise specified.
*
* Note that querystring arguments are also parsed and set as values in the returned configuration.
*
* @param string $dsn The DSN string to convert to a configuration array
* @return array<int|string, array|bool|string|null> The configuration array to be stored after parsing the DSN
* @throws \InvalidArgumentException If not passed a string, or passed an invalid string
*/
public static function parseDsn(string $dsn): array
{
if (!$dsn) {
return [];
}
$pattern = <<<'REGEXP'
{
^
(?P<_scheme>
(?P<scheme>[\w\\\\]+)://
)
(?P<_username>
(?P<username>.*?)
(?P<_password>
:(?P<password>.*?)
)?
@
)?
(?P<_host>
(?P<host>\[[^]]+]|[^?#/:@]+)
(?P<_port>
:(?P<port>\d+)
)?
)?
(?P<_path>
(?P<path>/[^?#]*)
)?
(?P<_query>
\?(?P<query>[^#]*)
)?
(?P<_fragment>
\#(?P<fragment>.*)
)?
$
}x
REGEXP;
preg_match($pattern, $dsn, $parsed);
if (!$parsed) {
throw new InvalidArgumentException(sprintf('The DSN string `%s` could not be parsed.', $dsn));
}
$exists = [];
/**
* @var string|int $k
*/
foreach ($parsed as $k => $v) {
if (is_int($k)) {
unset($parsed[$k]);
} elseif (str_starts_with($k, '_')) {
$exists[substr($k, 1)] = ($v !== '');
unset($parsed[$k]);
} elseif ($v === '' && !$exists[$k]) {
unset($parsed[$k]);
}
}
$query = '';
if (isset($parsed['query'])) {
$query = $parsed['query'];
unset($parsed['query']);
}
parse_str($query, $queryArgs);
/**
* @var string $key
*/
foreach ($queryArgs as $key => $value) {
if ($value === 'true') {
$queryArgs[$key] = true;
} elseif ($value === 'false') {
$queryArgs[$key] = false;
} elseif ($value === 'null') {
$queryArgs[$key] = null;
}
}
$parsed = $queryArgs + $parsed;
if (empty($parsed['className'])) {
$classMap = static::getDsnClassMap();
/** @var string $scheme */
$scheme = $parsed['scheme'];
$parsed['className'] = $scheme;
if (isset($classMap[$scheme])) {
$parsed['className'] = $classMap[$scheme];
}
}
return $parsed;
}
/**
* Updates the DSN class map for this class.
*
* @param array<string, string> $map Additions/edits to the class map to apply.
* @return void
* @phpstan-param array<string, class-string> $map
*/
public static function setDsnClassMap(array $map): void
{
static::$_dsnClassMap = $map + static::$_dsnClassMap;
}
/**
* Returns the DSN class map for this class.
*
* @return array<string, class-string>
*/
public static function getDsnClassMap(): array
{
return static::$_dsnClassMap;
}
}
@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 4.2.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\TestSuite;
use Cake\Core\Configure;
use Cake\Core\ConsoleApplicationInterface;
use Cake\Core\ContainerInterface;
use Cake\Core\HttpApplicationInterface;
use Cake\Event\EventInterface;
use Cake\Routing\Router;
use Closure;
use League\Container\Exception\NotFoundException;
use LogicException;
use PHPUnit\Framework\Attributes\After;
/**
* A set of methods used for defining container services
* in test cases.
*
* This trait leverages the `Application.buildContainer` event
* to inject the mocked services into the container that the
* application uses.
*/
trait ContainerStubTrait
{
/**
* The customized application class name.
*
* @phpstan-var class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface>|null
* @var string|null
*/
protected ?string $_appClass = null;
/**
* The customized application constructor arguments.
*
* @var array|null
*/
protected ?array $_appArgs = null;
/**
* The collection of container services.
*
* @var array<string, mixed>
*/
private array $containerServices = [];
/**
* Configure the application class to use in integration tests.
*
* @param string $class The application class name.
* @param array|null $constructorArgs The constructor arguments for your application class.
* @return void
* @phpstan-param class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface> $class
*/
public function configApplication(string $class, ?array $constructorArgs): void
{
$this->_appClass = $class;
$this->_appArgs = $constructorArgs;
}
/**
* Create an application instance.
*
* Uses the configuration set in `configApplication()`.
*
* @return \Cake\Core\HttpApplicationInterface|\Cake\Core\ConsoleApplicationInterface
*/
protected function createApp(): HttpApplicationInterface|ConsoleApplicationInterface
{
if (class_exists(Router::class)) {
Router::resetRoutes();
}
if ($this->_appClass) {
$appClass = $this->_appClass;
} else {
/** @var class-string<\Cake\Http\BaseApplication> $appClass */
$appClass = Configure::read('App.namespace') . '\Application';
}
if (!class_exists($appClass)) {
throw new LogicException(sprintf('Cannot load `%s` for use in integration testing.', $appClass));
}
$appArgs = $this->_appArgs ?: [CONFIG];
$app = new $appClass(...$appArgs);
if ($this->containerServices && method_exists($app, 'getEventManager')) {
$app->getEventManager()->on('Application.buildContainer', [$this, 'modifyContainer']);
}
foreach ($this->appPluginsToLoad as $pluginName => $config) {
if (is_array($config)) {
$app->addPlugin($pluginName, $config);
} else {
$app->addPlugin($config);
}
}
return $app;
}
/**
* Add a mocked service to the container.
*
* When the container is created the provided classname
* will be mapped to the factory function. The factory
* function will be used to create mocked services.
*
* @param string $class The class or interface you want to define.
* @param \Closure $factory The factory function for mocked services.
* @return $this
*/
public function mockService(string $class, Closure $factory)
{
$this->containerServices[$class] = $factory;
return $this;
}
/**
* Remove a mocked service to the container.
*
* @param string $class The class or interface you want to remove.
* @return $this
*/
public function removeMockService(string $class)
{
unset($this->containerServices[$class]);
return $this;
}
/**
* Wrap the application's container with one containing mocks.
*
* If any mocked services are defined, the application's container
* will be replaced with one containing mocks. The original
* container will be set as a delegate to the mock container.
*
* @param \Cake\Event\EventInterface $event The event
* @param \Cake\Core\ContainerInterface $container The container to wrap.
* @return void
*/
public function modifyContainer(EventInterface $event, ContainerInterface $container): void
{
if (!$this->containerServices) {
return;
}
foreach ($this->containerServices as $key => $factory) {
if ($container->has($key)) {
try {
$container->extend($key)->setConcrete($factory);
} catch (NotFoundException) {
$container->add($key, $factory);
}
} else {
$container->add($key, $factory);
}
}
$event->setResult($container);
}
/**
* Clears any mocks that were defined and cleans
* up application class configuration.
*
* @return void
*/
#[After]
public function cleanupContainer(): void
{
$this->_appArgs = null;
$this->_appClass = null;
$this->containerServices = [];
}
}
// phpcs:disable
class_alias(
'Cake\Core\TestSuite\ContainerStubTrait',
'Cake\TestSuite\ContainerStubTrait'
);
// phpcs:enable
+53
View File
@@ -0,0 +1,53 @@
{
"name": "cakephp/core",
"description": "CakePHP Framework Core classes",
"type": "library",
"keywords": [
"cakephp",
"framework",
"core"
],
"homepage": "https://cakephp.org",
"license": "MIT",
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"support": {
"issues": "https://github.com/cakephp/cakephp/issues",
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"source": "https://github.com/cakephp/core"
},
"require": {
"php": ">=8.2",
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"psr/container": "^1.1 || ^2.0"
},
"autoload": {
"psr-4": {
"Cake\\Core\\": "."
},
"files": [
"functions.php"
]
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"cakephp/cache": "To use Configure::store() and restore().",
"league/container": "To use Container and ServiceProvider classes"
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"branch-alias": {
"dev-5.next": "5.3.x-dev"
}
}
}
+539
View File
@@ -0,0 +1,539 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use JsonException;
use Stringable;
if (!defined('DS')) {
/**
* Defines DS as short form of DIRECTORY_SEPARATOR.
*/
define('DS', DIRECTORY_SEPARATOR);
}
if (!defined('CAKE_DATE_RFC7231')) {
define('CAKE_DATE_RFC7231', 'D, d M Y H:i:s \G\M\T');
}
if (!function_exists('Cake\Core\pathCombine')) {
/**
* Combines parts with a forward-slash `/`.
*
* Skips adding a forward-slash if either `/` or `\` already exists.
*
* @param array<string> $parts
* @param bool|null $trailing Determines how trailing slashes are handled
* - If true, ensures a trailing forward-slash is added if one doesn't exist
* - If false, ensures any trailing slash is removed
* - if null, ignores trailing slashes
* @return string
*/
function pathCombine(array $parts, ?bool $trailing = null): string
{
$numParts = count($parts);
if ($numParts === 0) {
if ($trailing === true) {
return '/';
}
return '';
}
$path = $parts[0];
for ($i = 1; $i < $numParts; ++$i) {
$part = $parts[$i];
if ($part === '') {
continue;
}
if ($path[-1] === '/' || $path[-1] === '\\') {
if ($part[0] === '/' || $part[0] === '\\') {
$path .= substr($part, 1);
} else {
$path .= $part;
}
} elseif ($part[0] === '/' || $part[0] === '\\') {
$path .= $part;
} else {
$path .= '/' . $part;
}
}
if ($trailing === true) {
if ($path === '' || ($path[-1] !== '/' && $path[-1] !== '\\')) {
$path .= '/';
}
} elseif ($trailing === false) {
if ($path !== '' && ($path[-1] === '/' || $path[-1] === '\\')) {
$path = substr($path, 0, -1);
}
}
return $path;
}
}
if (!function_exists('Cake\Core\h')) {
/**
* Convenience method for htmlspecialchars.
*
* @param mixed $text Text to wrap through htmlspecialchars. Also works with arrays, and objects.
* Arrays will be mapped and have all their elements escaped. Objects will be string cast if they
* implement a `__toString` method. Otherwise, the class name will be used.
* Other scalar types will be returned unchanged.
* @param bool $double Encode existing html entities.
* @param string|null $charset Character set to use when escaping.
* Defaults to config value in `mb_internal_encoding()` or 'UTF-8'.
* @return mixed Wrapped text.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#h
*/
function h(mixed $text, bool $double = true, ?string $charset = null): mixed
{
if (is_string($text)) {
//optimize for strings
} elseif (is_array($text)) {
$texts = [];
foreach ($text as $k => $t) {
$texts[$k] = h($t, $double, $charset);
}
return $texts;
} elseif (is_object($text)) {
if ($text instanceof Stringable) {
$text = (string)$text;
} else {
$text = '(object)' . $text::class;
}
} elseif ($text === null || is_scalar($text)) {
return $text;
}
static $defaultCharset = false;
if ($defaultCharset === false) {
$defaultCharset = mb_internal_encoding() ?: 'UTF-8';
}
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, $charset ?: $defaultCharset, $double);
}
}
if (!function_exists('Cake\Core\pluginSplit')) {
/**
* Splits a dot syntax plugin name into its plugin and class name.
* If $name does not have a dot, then index 0 will be null.
*
* Commonly used like
* ```
* list($plugin, $name) = pluginSplit($name);
* ```
*
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
* @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pluginSplit
* @phpstan-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
if (str_contains($name, '.')) {
$parts = explode('.', $name, 2);
if ($dotAppend) {
$parts[0] .= '.';
}
/** @phpstan-var array{string, string} */
return $parts;
}
return [$plugin, $name];
}
}
if (!function_exists('Cake\Core\namespaceSplit')) {
/**
* Split the namespace from the classname.
*
* Commonly used like `list($namespace, $className) = namespaceSplit($class);`.
*
* @param string $class The full class name, ie `Cake\Core\App`.
* @return array{0: string, 1: string} Array with 2 indexes. 0 => namespace, 1 => classname.
*/
function namespaceSplit(string $class): array
{
$pos = strrpos($class, '\\');
if ($pos === false) {
return ['', $class];
}
return [substr($class, 0, $pos), substr($class, $pos + 1)];
}
}
if (!function_exists('Cake\Core\pr')) {
/**
* print_r() convenience function.
*
* In terminals this will act similar to using print_r() directly, when not run on CLI
* print_r() will also wrap `<pre>` tags around the output of given variable. Similar to debug().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pr
* @see debug()
*/
function pr(mixed $var): mixed
{
if (!Configure::read('debug')) {
return $var;
}
$template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '<pre class="pr">%s</pre>' : "\n%s\n\n";
printf($template, trim(print_r($var, true)));
return $var;
}
}
if (!function_exists('Cake\Core\pj')) {
/**
* JSON pretty print convenience function.
*
* In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
* will also wrap `<pre>` tags around the output of given variable. Similar to pr().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @see pr()
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pj
*/
function pj(mixed $var): mixed
{
if (!Configure::read('debug')) {
return $var;
}
$template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '<pre class="pj">%s</pre>' : "\n%s\n\n";
$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
printf($template, trim((string)json_encode($var, $flags)));
return $var;
}
}
if (!function_exists('Cake\Core\env')) {
/**
* Gets an environment variable from available sources, and provides emulation
* for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
* IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
* environment information.
*
* @param string $key Environment variable name.
* @param string|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|float|int|bool|null Environment variable setting.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#env
*/
function env(string $key, string|float|int|bool|null $default = null): string|float|int|bool|null
{
if ($key === 'HTTPS') {
if (isset($_SERVER['HTTPS'])) {
return !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
}
return str_starts_with((string)env('SCRIPT_URI'), 'https://');
}
if ($key === 'SCRIPT_NAME' && env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) {
$key = 'SCRIPT_URL';
}
$val = $_SERVER[$key] ?? $_ENV[$key] ?? null;
assert($val === null || is_scalar($val));
if ($val == null && getenv($key) !== false) {
$val = (string)getenv($key);
}
if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) {
$addr = env('HTTP_PC_REMOTE_ADDR');
if ($addr !== null) {
$val = $addr;
}
}
if ($val !== null) {
return $val;
}
switch ($key) {
case 'DOCUMENT_ROOT':
$name = (string)env('SCRIPT_NAME');
$filename = (string)env('SCRIPT_FILENAME');
$offset = 0;
if (!str_ends_with($name, '.php')) {
$offset = 4;
}
return substr($filename, 0, -(strlen($name) + $offset));
case 'PHP_SELF':
return str_replace((string)env('DOCUMENT_ROOT'), '', (string)env('SCRIPT_FILENAME'));
case 'CGI_MODE':
return PHP_SAPI === 'cgi';
}
return $default;
}
}
if (!function_exists('Cake\Core\triggerWarning')) {
/**
* Triggers an E_USER_WARNING.
*
* @param string $message The warning message.
* @return void
*/
function triggerWarning(string $message): void
{
trigger_error($message, E_USER_WARNING);
}
}
if (!function_exists('Cake\Core\deprecationWarning')) {
/**
* Helper method for outputting deprecation warnings
*
* @param string $version The version that added this deprecation warning.
* @param string $message The message to output as a deprecation warning.
* @param int $stackFrame The stack frame to include in the error. Defaults to 1
* as that should point to application/plugin code.
* @return void
*/
function deprecationWarning(string $version, string $message, int $stackFrame = 1): void
{
if (!(error_reporting() & E_USER_DEPRECATED)) {
return;
}
$trace = debug_backtrace();
if (isset($trace[$stackFrame])) {
$frame = $trace[$stackFrame];
$frame += ['file' => '[internal]', 'line' => '??'];
// Assuming we're installed in vendor/cakephp/cakephp/src/Core/functions.php
$root = dirname(__DIR__, 5);
if (defined('ROOT')) {
$root = ROOT;
}
$relative = str_replace(DIRECTORY_SEPARATOR, '/', substr($frame['file'], strlen($root) + 1));
$patterns = (array)Configure::read('Error.ignoredDeprecationPaths');
foreach ($patterns as $pattern) {
$pattern = str_replace(DIRECTORY_SEPARATOR, '/', $pattern);
if (fnmatch($pattern, $relative)) {
return;
}
}
$message = sprintf(
"Since %s: %s\n%s, line: %s\n" .
'You can disable all deprecation warnings by setting `Error.errorLevel` to ' .
'`E_ALL & ~E_USER_DEPRECATED`. Adding `%s` to `Error.ignoredDeprecationPaths` ' .
'in your `config/app.php` config will mute deprecations from that file only.',
$version,
$message,
$frame['file'],
$frame['line'],
$relative,
);
}
static $errors = [];
$checksum = hash('xxh128', $message);
$duplicate = (bool)Configure::read('Error.allowDuplicateDeprecations', false);
if (isset($errors[$checksum]) && !$duplicate) {
return;
}
if (!$duplicate) {
$errors[$checksum] = true;
}
trigger_error($message, E_USER_DEPRECATED);
}
}
if (!function_exists('Cake\Core\toString')) {
/**
* Converts the given value to a string.
*
* This method attempts to convert the given value to a string.
* If the value is already a string, it returns the value as it is.
* ``null`` is returned if the conversion is not possible.
*
* @param mixed $value The value to be converted.
* @return ?string Returns the string representation of the value, or null if the value is not a string.
* @since 5.1.0
*/
function toString(mixed $value): ?string
{
if (is_string($value)) {
return $value;
}
if (is_int($value)) {
return (string)$value;
}
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_float($value)) {
if (is_nan($value) || is_infinite($value)) {
return null;
}
try {
$return = json_encode($value, JSON_THROW_ON_ERROR);
} catch (JsonException) {
$return = null;
}
if ($return === null || str_contains($return, 'e')) {
return rtrim(sprintf('%.' . (PHP_FLOAT_DIG + 3) . 'F', $value), '.0');
}
return $return;
}
if ($value instanceof Stringable) {
return (string)$value;
}
return null;
}
}
if (!function_exists('Cake\Core\toInt')) {
/**
* Converts a value to an integer.
*
* This method attempts to convert the given value to an integer.
* If the conversion is successful, it returns the value as an integer.
* If the conversion fails, it returns NULL.
*
* String values are trimmed using trim().
*
* @param mixed $value The value to be converted to an integer.
* @return int|null Returns the converted integer value or null if the conversion fails.
* @since 5.1.0
*/
function toInt(mixed $value): ?int
{
if (is_int($value)) {
return $value;
}
if (is_string($value)) {
$value = trim($value);
if (preg_match('/^0+[^0]{1}/', $value)) {
$value = ltrim($value, '0');
}
$value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
return $value === PHP_INT_MIN ? null : $value;
}
if (is_float($value)) {
if (is_nan($value) || is_infinite($value)) {
return null;
}
return (int)$value;
}
if (is_bool($value)) {
return (int)$value;
}
return null;
}
}
if (!function_exists('Cake\Core\toFloat')) {
/**
* Converts a value to a float.
*
* This method attempts to convert the given value to a float.
* If the conversion is successful, it returns the value as an float.
* If the conversion fails, it returns NULL.
*
* String values are trimmed using trim().
*
* @param mixed $value The value to be converted to a float.
* @return float|null Returns the converted float value or null if the conversion fails.
* @since 5.1.0
*/
function toFloat(mixed $value): ?float
{
if (is_string($value)) {
$value = trim($value);
if (preg_match('/^0+[^0]{1}/', $value)) {
$value = ltrim($value, '0');
}
$value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
return $value === PHP_FLOAT_MIN ? null : $value;
}
if (is_float($value)) {
if (is_nan($value) || is_infinite($value)) {
return null;
}
return $value;
}
if (is_int($value)) {
return (float)$value;
}
if (is_bool($value)) {
return (float)$value;
}
return null;
}
}
if (!function_exists('Cake\Core\toBool')) {
/**
* Converts a value to boolean.
*
* 1 | '1' | 1.0 | true - values returns as true
* 0 | '0' | 0.0 | false - values returns as false
* Other values returns as null.
*
* @param mixed $value The value to convert to boolean.
* @return bool|null Returns true if the value is truthy, false if it's falsy, or NULL otherwise.
* @since 5.1.0
*/
function toBool(mixed $value): ?bool
{
if (in_array($value, ['1', 1, 1.0, true], true)) {
return true;
}
if (in_array($value, ['0', 0, 0.0, false], true)) {
return false;
}
return null;
}
}
+271
View File
@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
// phpcs:disable PSR1.Files.SideEffects
use function Cake\Core\deprecationWarning as cakeDeprecationWarning;
use function Cake\Core\env as cakeEnv;
use function Cake\Core\h as cakeH;
use function Cake\Core\namespaceSplit as cakeNamespaceSplit;
use function Cake\Core\pathCombine as cakePathCombine;
use function Cake\Core\pj as cakePj;
use function Cake\Core\pluginSplit as cakePluginSplit;
use function Cake\Core\pr as cakePr;
use function Cake\Core\toBool as cakeToBool;
use function Cake\Core\toFloat as cakeToFloat;
use function Cake\Core\toInt as cakeToInt;
use function Cake\Core\toString as cakeToString;
use function Cake\Core\triggerWarning as cakeTriggerWarning;
if (!function_exists('pathCombine')) {
/**
* Combines parts with a forward-slash `/`.
*
* Skips adding a forward-slash if either `/` or `\` already exists.
*
* @param array<string> $parts
* @param bool|null $trailing Determines how trailing slashes are handled
* - If true, ensures a trailing forward-slash is added if one doesn't exist
* - If false, ensures any trailing slash is removed
* - if null, ignores trailing slashes
* @return string
*/
function pathCombine(array $parts, ?bool $trailing = null): string
{
return cakePathCombine($parts, $trailing);
}
}
if (!function_exists('h')) {
/**
* Convenience method for htmlspecialchars.
*
* @param mixed $text Text to wrap through htmlspecialchars. Also works with arrays, and objects.
* Arrays will be mapped and have all their elements escaped. Objects will be string cast if they
* implement a `__toString` method. Otherwise, the class name will be used.
* Other scalar types will be returned unchanged.
* @param bool $double Encode existing html entities.
* @param string|null $charset Character set to use when escaping.
* Defaults to config value in `mb_internal_encoding()` or 'UTF-8'.
* @return mixed Wrapped text.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#h
*/
function h(mixed $text, bool $double = true, ?string $charset = null): mixed
{
return cakeH($text, $double, $charset);
}
}
if (!function_exists('pluginSplit')) {
/**
* Splits a dot syntax plugin name into its plugin and class name.
* If $name does not have a dot, then index 0 will be null.
*
* Commonly used like
* ```
* list($plugin, $name) = pluginSplit($name);
* ```
*
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
* @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pluginSplit
* @phpstan-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
return cakePluginSplit($name, $dotAppend, $plugin);
}
}
if (!function_exists('namespaceSplit')) {
/**
* Split the namespace from the classname.
*
* Commonly used like `list($namespace, $className) = namespaceSplit($class);`.
*
* @param string $class The full class name, ie `Cake\Core\App`.
* @return array{0: string, 1: string} Array with 2 indexes. 0 => namespace, 1 => classname.
*/
function namespaceSplit(string $class): array
{
return cakeNamespaceSplit($class);
}
}
if (!function_exists('pr')) {
/**
* print_r() convenience function.
*
* In terminals this will act similar to using print_r() directly, when not run on CLI
* print_r() will also wrap `<pre>` tags around the output of given variable. Similar to debug().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pr
* @see debug()
*/
function pr(mixed $var): mixed
{
return cakePr($var);
}
}
if (!function_exists('pj')) {
/**
* JSON pretty print convenience function.
*
* In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
* will also wrap `<pre>` tags around the output of given variable. Similar to pr().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @see pr()
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pj
*/
function pj(mixed $var): mixed
{
return cakePj($var);
}
}
if (!function_exists('env')) {
/**
* Gets an environment variable from available sources, and provides emulation
* for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
* IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
* environment information.
*
* @param string $key Environment variable name.
* @param string|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|float|int|bool|null Environment variable setting.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#env
*/
function env(string $key, string|float|int|bool|null $default = null): string|float|int|bool|null
{
return cakeEnv($key, $default);
}
}
if (!function_exists('triggerWarning')) {
/**
* Triggers an E_USER_WARNING.
*
* @param string $message The warning message.
* @return void
*/
function triggerWarning(string $message): void
{
cakeTriggerWarning($message);
}
}
if (!function_exists('deprecationWarning')) {
/**
* Helper method for outputting deprecation warnings
*
* @param string $version The version that added this deprecation warning.
* @param string $message The message to output as a deprecation warning.
* @param int $stackFrame The stack frame to include in the error. Defaults to 1
* as that should point to application/plugin code.
* @return void
*/
function deprecationWarning(string $version, string $message, int $stackFrame = 1): void
{
cakeDeprecationWarning($version, $message, $stackFrame + 1);
}
}
if (!function_exists('toString')) {
/**
* Converts the given value to a string.
*
* This method attempts to convert the given value to a string.
* If the value is already a string, it returns the value as it is.
* ``null`` is returned if the conversion is not possible.
*
* @param mixed $value The value to be converted.
* @return ?string Returns the string representation of the value, or null if the value is not a string.
* @since 5.1.1
*/
function toString(mixed $value): ?string
{
return cakeToString($value);
}
}
if (!function_exists('toInt')) {
/**
* Converts a value to an integer.
*
* This method attempts to convert the given value to an integer.
* If the conversion is successful, it returns the value as an integer.
* If the conversion fails, it returns NULL.
*
* String values are trimmed using trim().
*
* @param mixed $value The value to be converted to an integer.
* @return int|null Returns the converted integer value or null if the conversion fails.
* @since 5.1.1
*/
function toInt(mixed $value): ?int
{
return cakeToInt($value);
}
}
if (!function_exists('toFloat')) {
/**
* Converts a value to a float.
*
* This method attempts to convert the given value to a float.
* If the conversion is successful, it returns the value as an float.
* If the conversion fails, it returns NULL.
*
* String values are trimmed using trim().
*
* @param mixed $value The value to be converted to a float.
* @return float|null Returns the converted float value or null if the conversion fails.
* @since 5.1.1
*/
function toFloat(mixed $value): ?float
{
return cakeToFloat($value);
}
}
if (!function_exists('toBool')) {
/**
* Converts a value to boolean.
*
* 1 | '1' | 1.0 | true - values returns as true
* 0 | '0' | 0.0 | false - values returns as false
* Other values returns as null.
*
* @param mixed $value The value to convert to boolean.
* @return bool|null Returns true if the value is truthy, false if it's falsy, or NULL otherwise.
* @since 5.1.1
*/
function toBool(mixed $value): ?bool
{
return cakeToBool($value);
}
}
+833
View File
@@ -0,0 +1,833 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Cache\Cache;
use Cake\Core\App;
use Cake\Core\Exception\CakeException;
use Cake\Core\Retry\CommandRetry;
use Cake\Database\Exception\MissingDriverException;
use Cake\Database\Exception\MissingExtensionException;
use Cake\Database\Exception\NestedTransactionRollbackException;
use Cake\Database\Query\DeleteQuery;
use Cake\Database\Query\InsertQuery;
use Cake\Database\Query\QueryFactory;
use Cake\Database\Query\SelectQuery;
use Cake\Database\Query\UpdateQuery;
use Cake\Database\Retry\ReconnectStrategy;
use Cake\Database\Schema\CachedCollection;
use Cake\Database\Schema\Collection as SchemaCollection;
use Cake\Database\Schema\CollectionInterface as SchemaCollectionInterface;
use Cake\Datasource\ConnectionInterface;
use Cake\Log\Log;
use Closure;
use Psr\SimpleCache\CacheInterface;
use Throwable;
use function Cake\Core\env;
/**
* Represents a connection with a database server.
*/
class Connection implements ConnectionInterface
{
/**
* Contains the configuration params for this connection.
*
* @var array<string, mixed>
*/
protected array $_config;
/**
* @var \Cake\Database\Driver
*/
protected Driver $readDriver;
/**
* @var \Cake\Database\Driver
*/
protected Driver $writeDriver;
/**
* Contains how many nested transactions have been started.
*
* @var int
*/
protected int $_transactionLevel = 0;
/**
* Whether a transaction is active in this connection.
*
* @var bool
*/
protected bool $_transactionStarted = false;
/**
* Whether this connection can and should use savepoints for nested
* transactions.
*
* @var bool
*/
protected bool $_useSavePoints = false;
/**
* Cacher object instance.
*
* @var \Psr\SimpleCache\CacheInterface|null
*/
protected ?CacheInterface $cacher = null;
/**
* The schema collection object
*
* @var \Cake\Database\Schema\CollectionInterface|null
*/
protected ?SchemaCollectionInterface $_schemaCollection = null;
/**
* NestedTransactionRollbackException object instance, will be stored if
* the rollback method is called in some nested transaction.
*
* @var \Cake\Database\Exception\NestedTransactionRollbackException|null
*/
protected ?NestedTransactionRollbackException $nestedTransactionRollbackException = null;
protected QueryFactory $queryFactory;
/**
* Constructor.
*
* ### Available options:
*
* - `driver` Sort name or FQCN for driver.
* - `log` Boolean indicating whether to use query logging.
* - `name` Connection name.
* - `cacheMetaData` Boolean indicating whether metadata (datasource schemas) should be cached.
* If set to a string it will be used as the name of cache config to use.
* - `cacheKeyPrefix` Custom prefix to use when generation cache keys. Defaults to connection name.
*
* @param array<string, mixed> $config Configuration array.
* @throws \Cake\Database\Exception\MissingDriverException when the driver class cannot be found
* @throws \Cake\Database\Exception\MissingExtensionException when the database extension is not enabled
*/
public function __construct(array $config)
{
$this->_config = $config;
[self::ROLE_READ => $this->readDriver, self::ROLE_WRITE => $this->writeDriver] = $this->createDrivers($config);
}
/**
* Creates read and write drivers.
*
* @param array<string, mixed> $config Connection config
* @return array<string, \Cake\Database\Driver>
* @phpstan-return array{read: \Cake\Database\Driver, write: \Cake\Database\Driver}
*/
protected function createDrivers(array $config): array
{
$driver = $config['driver'] ?? '';
if (!is_string($driver)) {
assert($driver instanceof Driver);
if (!$driver->enabled()) {
throw new MissingExtensionException(['driver' => $driver::class, 'name' => $this->configName()]);
}
// Legacy support for setting instance instead of driver class
return [self::ROLE_READ => $driver, self::ROLE_WRITE => $driver];
}
/** @var class-string<\Cake\Database\Driver>|null $driverClass */
$driverClass = App::className($driver, 'Database/Driver');
if ($driverClass === null) {
throw new MissingDriverException(['driver' => $driver, 'connection' => $this->configName()]);
}
$sharedConfig = array_diff_key($config, array_flip([
'name',
'className',
'driver',
'cacheMetaData',
'cacheKeyPrefix',
'read',
'write',
]));
$writeConfig = ($config['write'] ?? []) + $sharedConfig;
$readConfig = ($config['read'] ?? []) + $sharedConfig;
if (array_key_exists('write', $config) || array_key_exists('read', $config)) {
$readDriver = new $driverClass(['_role' => self::ROLE_READ] + $readConfig);
$writeDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
} else {
$readDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
$writeDriver = $readDriver;
}
if (!$writeDriver->enabled()) {
throw new MissingExtensionException(['driver' => $writeDriver::class, 'name' => $this->configName()]);
}
return [self::ROLE_READ => $readDriver, self::ROLE_WRITE => $writeDriver];
}
/**
* Destructor
*
* Disconnects the driver to release the connection.
*/
public function __destruct()
{
if ($this->_transactionStarted && class_exists(Log::class)) {
$message = 'The connection is going to be closed but there is an active transaction.';
$requestUrl = env('REQUEST_URI');
if ($requestUrl) {
$message .= "\nRequest URL: " . $requestUrl;
}
$clientIp = env('REMOTE_ADDR');
if ($clientIp) {
$message .= "\nClient IP: " . $clientIp;
}
Log::warning($message);
}
}
/**
* @inheritDoc
*/
public function config(): array
{
return $this->_config;
}
/**
* @inheritDoc
*/
public function configName(): string
{
return $this->_config['name'] ?? '';
}
/**
* Returns the connection role: read or write.
*
* @return string
*/
public function role(): string
{
return preg_match('/:read$/', $this->configName()) === 1 ? static::ROLE_READ : static::ROLE_WRITE;
}
/**
* Get the retry wrapper object that is allows recovery from server disconnects
* while performing certain database actions, such as executing a query.
*
* @return \Cake\Core\Retry\CommandRetry The retry wrapper
*/
public function getDisconnectRetry(): CommandRetry
{
return new CommandRetry(new ReconnectStrategy($this));
}
/**
* Gets the role-specific driver instance.
*
* @param string $role Connection role ('read' or 'write')
* @return \Cake\Database\Driver
*/
public function getDriver(string $role = self::ROLE_WRITE): Driver
{
assert($role === self::ROLE_READ || $role === self::ROLE_WRITE);
return $role === self::ROLE_READ ? $this->getReadDriver() : $this->getWriteDriver();
}
/**
* Gets the read-role driver instance.
*
* @return \Cake\Database\Driver
*/
public function getReadDriver(): Driver
{
return $this->readDriver;
}
/**
* Gets the write-role driver instance.
*
* @return \Cake\Database\Driver
*/
public function getWriteDriver(): Driver
{
return $this->writeDriver;
}
/**
* Executes a query using $params for interpolating values and $types as a hint for each
* those params.
*
* @param string $sql SQL to be executed and interpolated with $params
* @param array $params list or associative array of params to be interpolated in $sql as values
* @param array $types list or associative array of types to be used for casting values in query
* @return \Cake\Database\StatementInterface executed statement
*/
public function execute(string $sql, array $params = [], array $types = []): StatementInterface
{
return $this->getDisconnectRetry()->run(fn() => $this->getWriteDriver()->execute($sql, $params, $types));
}
/**
* Executes the provided query after compiling it for the specific driver
* dialect and returns the executed Statement object.
*
* @param \Cake\Database\Query $query The query to be executed
* @return \Cake\Database\StatementInterface executed statement
*/
public function run(Query $query): StatementInterface
{
return $this->getDisconnectRetry()->run(fn() => $this->getDriver($query->getConnectionRole())->run($query));
}
/**
* Get query factory instance.
*
* @return \Cake\Database\Query\QueryFactory
*/
public function queryFactory(): QueryFactory
{
return $this->queryFactory ??= new QueryFactory($this);
}
/**
* Create a new SelectQuery instance for this connection.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields Fields/columns list for the query.
* @param array|string $table The table or list of tables to query.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\SelectQuery<mixed>
*/
public function selectQuery(
ExpressionInterface|Closure|array|string|float|int $fields = [],
array|string $table = [],
array $types = [],
): SelectQuery {
return $this->queryFactory()->select($fields, $table, $types);
}
/**
* Create a new InsertQuery instance for this connection.
*
* @param string|null $table The table to insert rows into.
* @param array $values Associative array of column => value to be inserted.
* @param array<int|string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\InsertQuery
*/
public function insertQuery(?string $table = null, array $values = [], array $types = []): InsertQuery
{
return $this->queryFactory()->insert($table, $values, $types);
}
/**
* Create a new UpdateQuery instance for this connection.
*
* @param \Cake\Database\ExpressionInterface|string|null $table The table to update rows of.
* @param array $values Values to be updated.
* @param array $conditions Conditions to be set for the update statement.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\UpdateQuery
*/
public function updateQuery(
ExpressionInterface|string|null $table = null,
array $values = [],
array $conditions = [],
array $types = [],
): UpdateQuery {
return $this->queryFactory()->update($table, $values, $conditions, $types);
}
/**
* Create a new DeleteQuery instance for this connection.
*
* @param string|null $table The table to delete rows from.
* @param array $conditions Conditions to be set for the delete statement.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\DeleteQuery
*/
public function deleteQuery(?string $table = null, array $conditions = [], array $types = []): DeleteQuery
{
return $this->queryFactory()->delete($table, $conditions, $types);
}
/**
* Sets a Schema\Collection object for this connection.
*
* @param \Cake\Database\Schema\CollectionInterface $collection The schema collection object
* @return $this
*/
public function setSchemaCollection(SchemaCollectionInterface $collection)
{
$this->_schemaCollection = $collection;
return $this;
}
/**
* Gets a Schema\Collection object for this connection.
*
* @return \Cake\Database\Schema\CollectionInterface
*/
public function getSchemaCollection(): SchemaCollectionInterface
{
if ($this->_schemaCollection !== null) {
return $this->_schemaCollection;
}
if (!empty($this->_config['cacheMetadata'])) {
return $this->_schemaCollection = new CachedCollection(
new SchemaCollection($this),
empty($this->_config['cacheKeyPrefix']) ? $this->configName() : $this->_config['cacheKeyPrefix'],
$this->getCacher(),
);
}
return $this->_schemaCollection = new SchemaCollection($this);
}
/**
* Executes an INSERT query on the specified table.
*
* @param string $table the table to insert values in
* @param array $values values to be inserted
* @param array<string, string> $types Array containing the types to be used for casting
* @return \Cake\Database\StatementInterface
*/
public function insert(string $table, array $values, array $types = []): StatementInterface
{
return $this->insertQuery($table, $values, $types)->execute();
}
/**
* Executes an UPDATE statement on the specified table.
*
* @param string $table the table to update rows from
* @param array $values values to be updated
* @param array $conditions conditions to be set for update statement
* @param array<string, string> $types list of associative array containing the types to be used for casting
* @return \Cake\Database\StatementInterface
*/
public function update(string $table, array $values, array $conditions = [], array $types = []): StatementInterface
{
return $this->updateQuery($table, $values, $conditions, $types)->execute();
}
/**
* Executes a DELETE statement on the specified table.
*
* @param string $table the table to delete rows from
* @param array $conditions conditions to be set for delete statement
* @param array<string, string> $types list of associative array containing the types to be used for casting
* @return \Cake\Database\StatementInterface
*/
public function delete(string $table, array $conditions = [], array $types = []): StatementInterface
{
return $this->deleteQuery($table, $conditions, $types)->execute();
}
/**
* Starts a new transaction.
*
* @return void
*/
public function begin(): void
{
if (!$this->_transactionStarted) {
$this->getDisconnectRetry()->run(function (): void {
$this->getWriteDriver()->beginTransaction();
});
$this->_transactionLevel = 0;
$this->_transactionStarted = true;
$this->nestedTransactionRollbackException = null;
return;
}
$this->_transactionLevel++;
if ($this->isSavePointsEnabled()) {
$this->createSavePoint((string)$this->_transactionLevel);
}
}
/**
* Commits current transaction.
*
* @return bool true on success, false otherwise
* @throws \Cake\Database\Exception\NestedTransactionRollbackException when a nested transaction was rolled back
*/
public function commit(): bool
{
if (!$this->_transactionStarted) {
return false;
}
if ($this->_transactionLevel === 0) {
if ($this->wasNestedTransactionRolledback()) {
$e = $this->nestedTransactionRollbackException;
assert($e !== null);
$this->nestedTransactionRollbackException = null;
throw $e;
}
$this->_transactionStarted = false;
$this->nestedTransactionRollbackException = null;
return $this->getWriteDriver()->commitTransaction();
}
if ($this->isSavePointsEnabled()) {
$this->releaseSavePoint((string)$this->_transactionLevel);
}
$this->_transactionLevel--;
return true;
}
/**
* Rollback current transaction.
*
* @param bool|null $toBeginning Whether the transaction should be rolled back to the
* beginning of it. Defaults to false if using savepoints, or true if not.
* @return bool
*/
public function rollback(?bool $toBeginning = null): bool
{
if (!$this->_transactionStarted) {
return false;
}
$useSavePoint = $this->isSavePointsEnabled();
$toBeginning ??= !$useSavePoint;
if ($this->_transactionLevel === 0 || $toBeginning) {
$this->_transactionLevel = 0;
$this->_transactionStarted = false;
$this->nestedTransactionRollbackException = null;
$this->getWriteDriver()->rollbackTransaction();
return true;
}
$savePoint = $this->_transactionLevel--;
if ($useSavePoint) {
$this->rollbackSavepoint($savePoint);
} else {
$this->nestedTransactionRollbackException ??= new NestedTransactionRollbackException();
}
return true;
}
/**
* Enables/disables the usage of savepoints, enables only if the driver allows it.
*
* If you are trying to enable this feature, make sure you check
* `isSavePointsEnabled()` to verify that savepoints were enabled successfully.
*
* @param bool $enable Whether save points should be used.
* @return $this
*/
public function enableSavePoints(bool $enable = true)
{
if ($enable === false) {
$this->_useSavePoints = false;
} else {
$this->_useSavePoints = $this->getWriteDriver()->supports(DriverFeatureEnum::SAVEPOINT);
}
return $this;
}
/**
* Disables the usage of savepoints.
*
* @return $this
*/
public function disableSavePoints()
{
$this->_useSavePoints = false;
return $this;
}
/**
* Returns whether this connection is using savepoints for nested transactions
*
* @return bool true if enabled, false otherwise
*/
public function isSavePointsEnabled(): bool
{
return $this->_useSavePoints;
}
/**
* Creates a new save point for nested transactions.
*
* @param string|int $name Save point name or id
* @return void
*/
public function createSavePoint(string|int $name): void
{
$this->execute($this->getWriteDriver()->savePointSQL($name));
}
/**
* Releases a save point by its name.
*
* @param string|int $name Save point name or id
* @return void
*/
public function releaseSavePoint(string|int $name): void
{
$sql = $this->getWriteDriver()->releaseSavePointSQL($name);
if ($sql) {
$this->execute($sql);
}
}
/**
* Rollback a save point by its name.
*
* @param string|int $name Save point name or id
* @return void
*/
public function rollbackSavepoint(string|int $name): void
{
$this->execute($this->getWriteDriver()->rollbackSavePointSQL($name));
}
/**
* Run driver specific SQL to disable foreign key checks.
*
* @return void
*/
public function disableForeignKeys(): void
{
$this->getDisconnectRetry()->run(function (): void {
$this->execute($this->getWriteDriver()->disableForeignKeySQL());
});
}
/**
* Run driver specific SQL to enable foreign key checks.
*
* @return void
*/
public function enableForeignKeys(): void
{
$this->getDisconnectRetry()->run(function (): void {
$this->execute($this->getWriteDriver()->enableForeignKeySQL());
});
}
/**
* Executes a callback inside a transaction, if any exception occurs
* while executing the passed callback, the transaction will be rolled back
* If the result of the callback is `false`, the transaction will
* also be rolled back. Otherwise the transaction is committed after executing
* the callback.
*
* The callback will receive the connection instance as its first argument.
*
* ### Example:
*
* ```
* $connection->transactional(function ($connection) {
* $connection->deleteQuery('users')->execute();
* });
* ```
*
* @param \Closure $callback The callback to execute within a transaction.
* @return mixed The return value of the callback.
* @throws \Exception Will re-throw any exception raised in $callback after
* rolling back the transaction.
*/
public function transactional(Closure $callback): mixed
{
$this->begin();
try {
$result = $callback($this);
} catch (Throwable $e) {
$this->rollback(false);
throw $e;
}
if ($result === false) {
$this->rollback(false);
return false;
}
try {
$this->commit();
} catch (NestedTransactionRollbackException $e) {
$this->rollback(false);
throw $e;
}
return $result;
}
/**
* Returns whether some nested transaction has been already rolled back.
*
* @return bool
*/
protected function wasNestedTransactionRolledback(): bool
{
return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException;
}
/**
* Run an operation with constraints disabled.
*
* Constraints should be re-enabled after the callback succeeds/fails.
*
* ### Example:
*
* ```
* $connection->disableConstraints(function ($connection) {
* $connection->insertQuery('users')->execute();
* });
* ```
*
* @param \Closure $callback Callback to run with constraints disabled
* @return mixed The return value of the callback.
* @throws \Exception Will re-throw any exception raised in $callback after
* rolling back the transaction.
*/
public function disableConstraints(Closure $callback): mixed
{
return $this->getDisconnectRetry()->run(function () use ($callback) {
$this->disableForeignKeys();
try {
$result = $callback($this);
} finally {
$this->enableForeignKeys();
}
return $result;
});
}
/**
* Checks if a transaction is running.
*
* @return bool True if a transaction is running else false.
*/
public function inTransaction(): bool
{
return $this->_transactionStarted;
}
/**
* Enables or disables metadata caching for this connection
*
* Changing this setting will not modify existing schema collections objects.
*
* @param string|bool $cache Either boolean false to disable metadata caching, or
* true to use `_cake_model_` or the name of the cache config to use.
* @return void
*/
public function cacheMetadata(string|bool $cache): void
{
$this->_schemaCollection = null;
$this->_config['cacheMetadata'] = $cache;
if (is_string($cache)) {
$this->cacher = null;
}
}
/**
* @inheritDoc
*/
public function setCacher(CacheInterface $cacher)
{
$this->cacher = $cacher;
return $this;
}
/**
* @inheritDoc
*/
public function getCacher(): CacheInterface
{
if ($this->cacher !== null) {
return $this->cacher;
}
$configName = $this->_config['cacheMetadata'] ?? '_cake_model_';
if (!is_string($configName)) {
$configName = '_cake_model_';
}
if (!class_exists(Cache::class)) {
throw new CakeException(
'To use caching you must either set a cacher using Connection::setCacher()' .
' or require the cakephp/cache package in your composer config.',
);
}
return $this->cacher = Cache::pool($configName);
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$secrets = [
'password' => '*****',
'username' => '*****',
'host' => '*****',
'database' => '*****',
'port' => '*****',
];
$replace = array_intersect_key($secrets, $this->_config);
$config = $replace + $this->_config;
if (isset($config['read'])) {
$config['read'] = array_intersect_key($secrets, $config['read']) + $config['read'];
}
if (isset($config['write'])) {
$config['write'] = array_intersect_key($secrets, $config['write']) + $config['write'];
}
return [
'config' => $config,
'readDriver' => $this->readDriver,
'writeDriver' => $this->writeDriver,
'transactionLevel' => $this->_transactionLevel,
'transactionStarted' => $this->_transactionStarted,
'useSavePoints' => $this->_useSavePoints,
];
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Datasource\ConnectionInterface;
/**
* Defines the interface for a fixture that needs to manage constraints.
*
* @deprecated 5.2.5 This interface is no longer used.
*/
interface ConstraintsInterface
{
/**
* Build and execute SQL queries necessary to create the constraints for the
* fixture
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
* into which the constraints will be created.
* @return bool on success or if there are no constraints to create, or false on failure
*/
public function createConstraints(ConnectionInterface $connection): bool;
/**
* Build and execute SQL queries necessary to drop the constraints for the
* fixture
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
* into which the constraints will be dropped.
* @return bool on success or if there are no constraints to drop, or false on failure
*/
public function dropConstraints(ConnectionInterface $connection): bool;
}
File diff suppressed because it is too large Load Diff
+337
View File
@@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\DriverFeatureEnum;
use Cake\Database\Query;
use Cake\Database\Query\SelectQuery;
use Cake\Database\Schema\MysqlSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\StatementInterface;
use PDO;
use Pdo\Mysql as PdoMysql;
/**
* MySQL Driver
*/
class Mysql extends Driver
{
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 256;
/**
* Server type MySQL
*
* @var string
*/
protected const SERVER_TYPE_MYSQL = 'mysql';
/**
* Server type MariaDB
*
* @var string
*/
protected const SERVER_TYPE_MARIADB = 'mariadb';
/**
* Base configuration settings for MySQL driver
*
* @var array<string, mixed>
*/
protected array $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'port' => '3306',
'flags' => [],
'encoding' => 'utf8mb4',
'timezone' => null,
'init' => [],
];
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '`';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = '`';
/**
* Server type.
*
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
* after `version()` method is called.
*
* @var string
*/
protected string $serverType = self::SERVER_TYPE_MYSQL;
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, array<string, string>>
*/
protected array $featureVersions = [
'mysql' => [
'json' => '5.7.0',
'cte' => '8.0.0',
'window' => '8.0.0',
'intersect' => '8.0.31',
'intersect-all' => '8.0.31',
'check-constraints' => '8.0.16',
],
'mariadb' => [
'json' => '10.2.7',
'cte' => '10.2.1',
'window' => '10.2.0',
'intersect' => '10.3.0',
'intersect-all' => '10.5.0',
'check-constraints' => '10.2.1',
],
];
/**
* @inheritDoc
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$config = $this->_config;
if ($config['timezone'] === 'UTC') {
$config['timezone'] = '+0:00';
}
if (!empty($config['timezone'])) {
$config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']);
}
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
$this->attrUseBufferedQueryId() => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
$config['flags'][$this->attrSslKeyId()] = $config['ssl_key'];
$config['flags'][$this->attrSslCertId()] = $config['ssl_cert'];
}
if (!empty($config['ssl_ca'])) {
$config['flags'][$this->attrSslCaId()] = $config['ssl_ca'];
}
if (empty($config['unix_socket'])) {
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
} else {
$dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}
if (!empty($config['encoding'])) {
$dsn .= ";charset={$config['encoding']}";
}
$this->pdo = $this->createPdo($dsn, $config);
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->pdo->exec($command);
}
}
}
/**
* @inheritDoc
*/
public function run(Query $query): StatementInterface
{
$statement = $this->prepare($query);
$query->getValueBinder()->attachTo($statement);
if ($query instanceof SelectQuery) {
try {
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), $query->isBufferedResultsEnabled());
$this->executeStatement($statement);
} finally {
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), true);
}
} else {
$this->executeStatement($statement);
}
return $statement;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('mysql', PDO::getAvailableDrivers(), true);
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ?? ($this->_schemaDialect = new MysqlSchemaDialect($this));
}
/**
* @inheritDoc
*/
public function schema(): string
{
return $this->_config['database'];
}
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string
{
return 'SET foreign_key_checks = 0';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET foreign_key_checks = 1';
}
/**
* @inheritDoc
*/
public function supports(DriverFeatureEnum $feature): bool
{
$versionCompare = function () use ($feature) {
return version_compare(
$this->version(),
$this->featureVersions[$this->serverType][$feature->value],
'>=',
);
};
return match ($feature) {
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
DriverFeatureEnum::SAVEPOINT => true,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => false,
DriverFeatureEnum::CTE,
DriverFeatureEnum::JSON,
DriverFeatureEnum::WINDOW => $versionCompare(),
DriverFeatureEnum::INTERSECT => $versionCompare(),
DriverFeatureEnum::INTERSECT_ALL => $versionCompare(),
DriverFeatureEnum::CHECK_CONSTRAINTS => $versionCompare(),
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
};
}
/**
* Returns true if the connected server is MariaDB.
*
* @return bool
*/
public function isMariadb(): bool
{
$this->version();
return $this->serverType === static::SERVER_TYPE_MARIADB;
}
/**
* Returns connected server version.
*
* @return string
*/
public function version(): string
{
if ($this->_version === null) {
$this->_version = (string)$this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
if (str_contains($this->_version, 'MariaDB')) {
$this->serverType = static::SERVER_TYPE_MARIADB;
preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches);
$this->_version = $matches[1];
}
}
return $this->_version;
}
/**
* Get PDO ATTR_SSL_KEY id.
*
* @return int
*/
private function attrSslKeyId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_KEY : PdoMysql::ATTR_SSL_KEY;
}
/**
* Get PDO ATTR_SSL_CERT id.
*
* @return int
*/
private function attrSslCertId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CERT : PdoMysql::ATTR_SSL_CERT;
}
/**
* Get PDO ATTR_SSL_CA id.
*
* @return int
*/
private function attrSslCaId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CA : PdoMysql::ATTR_SSL_CA;
}
/**
* Get PDO ATTR_USE_BUFFERED_QUERY id.
*
* @return int
*/
private function attrUseBufferedQueryId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_USE_BUFFERED_QUERY : PdoMysql::ATTR_USE_BUFFERED_QUERY;
}
}
+363
View File
@@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\DriverFeatureEnum;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\StringExpression;
use Cake\Database\PostgresCompiler;
use Cake\Database\Query\InsertQuery;
use Cake\Database\Query\SelectQuery;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\PostgresSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use PDO;
/**
* Class Postgres
*/
class Postgres extends Driver
{
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 63;
/**
* Base configuration settings for Postgres driver
*
* @var array<string, mixed>
*/
protected array $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'schema' => 'public',
'port' => 5432,
'encoding' => 'utf8',
'timezone' => null,
'flags' => [],
'init' => [],
'ssl_key' => null,
'ssl_cert' => null,
'ssl_ca' => null,
'ssl' => false,
'ssl_mode' => null,
];
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = '"';
/**
* @inheritDoc
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$config = $this->_config;
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (empty($config['unix_socket'])) {
$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
} else {
$dsn = "pgsql:dbname={$config['database']}";
}
if ($this->_config['ssl']) {
if ($this->_config['ssl_mode']) {
$dsn .= ';sslmode=' . $this->_config['ssl_mode'];
} else {
$dsn .= ';sslmode=allow';
}
if ($this->_config['ssl_key']) {
$dsn .= ';sslkey=' . $this->_config['ssl_key'];
}
if ($this->_config['ssl_cert']) {
$dsn .= ';sslcert=' . $this->_config['ssl_cert'];
}
if ($this->_config['ssl_ca']) {
$dsn .= ';sslrootcert=' . $this->_config['ssl_ca'];
}
}
$this->pdo = $this->createPdo($dsn, $config);
if (!empty($config['encoding'])) {
$this->setEncoding($config['encoding']);
}
if (!empty($config['schema'])) {
$this->setSchema($config['schema']);
}
if (!empty($config['timezone'])) {
$config['init'][] = sprintf('SET timezone = %s', $this->getPdo()->quote($config['timezone']));
}
foreach ($config['init'] as $command) {
/** @phpstan-ignore-next-line */
$this->pdo->exec($command);
}
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('pgsql', PDO::getAvailableDrivers(), true);
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ?? ($this->_schemaDialect = new PostgresSchemaDialect($this));
}
/**
* Sets connection encoding
*
* @param string $encoding The encoding to use.
* @return void
*/
public function setEncoding(string $encoding): void
{
$pdo = $this->getPdo();
$pdo->exec('SET NAMES ' . $pdo->quote($encoding));
}
/**
* Sets connection default schema, if any relation defined in a query is not fully qualified
* postgres will fallback to looking the relation into defined default schema
*
* @param string $schema The schema names to set `search_path` to.
* @return void
*/
public function setSchema(string $schema): void
{
$pdo = $this->getPdo();
$pdo->exec('SET search_path TO ' . $pdo->quote($schema));
}
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL DEFERRED';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL IMMEDIATE';
}
/**
* @inheritDoc
*/
public function supports(DriverFeatureEnum $feature): bool
{
return match ($feature) {
DriverFeatureEnum::CTE,
DriverFeatureEnum::JSON,
DriverFeatureEnum::SAVEPOINT,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
DriverFeatureEnum::WINDOW => true,
DriverFeatureEnum::INTERSECT => true,
DriverFeatureEnum::INTERSECT_ALL => true,
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION => false,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
};
}
/**
* @inheritDoc
*/
protected function _transformDistinct(SelectQuery $query): SelectQuery
{
return $query;
}
/**
* @inheritDoc
*/
protected function _insertQueryTranslator(InsertQuery $query): InsertQuery
{
if (!$query->clause('epilog')) {
$query->epilog('RETURNING *');
}
return $query;
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
IdentifierExpression::class => '_transformIdentifierExpression',
FunctionExpression::class => '_transformFunctionExpression',
StringExpression::class => '_transformStringExpression',
];
}
/**
* Changes identifer expression into postgresql format.
*
* @param \Cake\Database\Expression\IdentifierExpression $expression The expression to transform.
* @return void
*/
protected function _transformIdentifierExpression(IdentifierExpression $expression): void
{
$collation = $expression->getCollation();
if ($collation) {
// use trim() to work around expression being transformed multiple times
$expression->setCollation('"' . trim($collation, '"') . '"');
}
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
* to postgres SQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 || exp2
$expression->setName('')->setConjunction(' ||');
break;
case 'DATEDIFF':
$expression
->setName('')
->setConjunction('-')
->iterateParts(function ($p) {
if (is_string($p)) {
$p = ['value' => [$p => 'literal'], 'type' => null];
} else {
$p['value'] = [$p['value']];
}
return new FunctionExpression('DATE', $p['value'], [$p['type']]);
});
break;
case 'CURRENT_DATE':
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
break;
case 'CURRENT_TIME':
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
break;
case 'NOW':
$expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
break;
case 'RAND':
$expression->setName('RANDOM');
break;
case 'DATE_ADD':
$expression
->setName('')
->setConjunction(' + INTERVAL')
->iterateParts(function ($p, $key) {
if ($key === 1) {
return sprintf("'%s'", $p);
}
return $p;
});
break;
case 'DAYOFWEEK':
$expression
->setName('EXTRACT')
->setConjunction(' ')
->add(['DOW FROM' => 'literal'], [], true)
->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
break;
case 'JSON_VALUE':
$expression->setName('JSONB_PATH_QUERY')
->iterateParts(function ($p, $key) {
if ($key === 0) {
$p = sprintf('%s::jsonb', $p);
} elseif ($key === 1) {
$p = sprintf("'%s'::jsonpath", $this->quoteIdentifier($p['value']));
}
return $p;
});
break;
}
}
/**
* Changes string expression into postgresql format.
*
* @param \Cake\Database\Expression\StringExpression $expression The string expression to transform.
* @return void
*/
protected function _transformStringExpression(StringExpression $expression): void
{
// use trim() to work around expression being transformed multiple times
$expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\PostgresCompiler
*/
public function newCompiler(): QueryCompiler
{
return new PostgresCompiler();
}
}
+309
View File
@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\DriverFeatureEnum;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqliteSchemaDialect;
use Cake\Database\Statement\SqliteStatement;
use InvalidArgumentException;
use PDO;
/**
* Class Sqlite
*/
class Sqlite extends Driver
{
use TupleComparisonTranslatorTrait;
/**
* @inheritDoc
*/
protected const STATEMENT_CLASS = SqliteStatement::class;
/**
* Base configuration settings for Sqlite driver
*
* - `mask` The mask used for created database
*
* @var array<string, mixed>
*/
protected array $_baseConfig = [
'persistent' => false,
'username' => null,
'password' => null,
'database' => ':memory:',
'encoding' => 'utf8',
'mask' => 0644,
'cache' => null,
'mode' => null,
'flags' => [],
'init' => [],
];
/**
* Whether the connected server supports window functions.
*
* @var bool|null
*/
protected ?bool $_supportsWindowFunctions = null;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = '"';
/**
* Mapping of date parts.
*
* @var array<string, string>
*/
protected array $_dateParts = [
'day' => 'd',
'hour' => 'H',
'month' => 'm',
'minute' => 'M',
'second' => 'S',
'week' => 'W',
'year' => 'Y',
];
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, string>
*/
protected array $featureVersions = [
'cte' => '3.8.3',
'window' => '3.28.0',
];
/**
* @inheritDoc
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$config = $this->_config;
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!is_string($config['database']) || $config['database'] === '') {
$name = $config['name'] ?? 'unknown';
throw new InvalidArgumentException(
"The `database` key for the `{$name}` SQLite connection needs to be a non-empty string.",
);
}
$chmodFile = false;
if ($config['database'] !== ':memory:' && $config['mode'] !== 'memory') {
$chmodFile = !file_exists($config['database']);
}
$params = [];
if ($config['cache']) {
$params[] = 'cache=' . $config['cache'];
}
if ($config['mode']) {
$params[] = 'mode=' . $config['mode'];
}
if ($params) {
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
} else {
$dsn = 'sqlite:' . $config['database'];
}
$this->pdo = $this->createPdo($dsn, $config);
if ($chmodFile) {
// phpcs:disable
@chmod($config['database'], $config['mask']);
// phpcs:enable
}
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->pdo->exec($command);
}
}
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('sqlite', PDO::getAvailableDrivers(), true);
}
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = OFF';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = ON';
}
/**
* @inheritDoc
*/
public function supports(DriverFeatureEnum $feature): bool
{
return match ($feature) {
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
DriverFeatureEnum::SAVEPOINT,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => true,
DriverFeatureEnum::JSON => false,
DriverFeatureEnum::CTE,
DriverFeatureEnum::WINDOW => version_compare(
$this->version(),
$this->featureVersions[$feature->value],
'>=',
),
DriverFeatureEnum::INTERSECT => true,
DriverFeatureEnum::INTERSECT_ALL => false,
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
};
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ?? ($this->_schemaDialect = new SqliteSchemaDialect($this));
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
TupleComparison::class => '_transformTupleComparison',
];
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 || exp2
$expression->setName('')->setConjunction(' ||');
break;
case 'DATEDIFF':
$expression
->setName('ROUND')
->setConjunction('-')
->iterateParts(function ($p) {
return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]);
});
break;
case 'NOW':
$expression->setName('DATETIME')->add(["'now'" => 'literal']);
break;
case 'RAND':
$expression
->setName('ABS')
->add(['RANDOM() % 1' => 'literal'], [], true);
break;
case 'CURRENT_DATE':
$expression->setName('DATE')->add(["'now'" => 'literal']);
break;
case 'CURRENT_TIME':
$expression->setName('TIME')->add(["'now'" => 'literal']);
break;
case 'EXTRACT':
$expression
->setName('STRFTIME')
->setConjunction(' ,')
->iterateParts(function ($p, $key) {
if ($key === 0) {
$value = rtrim(strtolower($p), 's');
if (isset($this->_dateParts[$value])) {
$p = ['value' => '%' . $this->_dateParts[$value], 'type' => null];
}
}
return $p;
});
break;
case 'DATE_ADD':
$expression
->setName('DATE')
->setConjunction(',')
->iterateParts(function ($p, $key) {
if ($key === 1) {
return ['value' => $p, 'type' => null];
}
return $p;
});
break;
case 'DAYOFWEEK':
$expression
->setName('STRFTIME')
->setConjunction(' ')
->add(["'%w', " => 'literal'], [], true)
->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1
break;
case 'JSON_VALUE':
$expression->setName('JSON_EXTRACT');
break;
}
}
}
+561
View File
@@ -0,0 +1,561 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\DriverFeatureEnum;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\OrderByExpression;
use Cake\Database\Expression\OrderClauseExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Expression\UnaryExpression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Query\SelectQuery;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqlserverSchemaDialect;
use Cake\Database\SqlserverCompiler;
use Cake\Database\Statement\SqlserverStatement;
use Cake\Database\StatementInterface;
use InvalidArgumentException;
use PDO;
/**
* SQLServer driver.
*/
class Sqlserver extends Driver
{
use TupleComparisonTranslatorTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 128;
/**
* @inheritDoc
*/
protected const RETRY_ERROR_CODES = [
40613, // Azure Sql Database paused
];
/**
* @inheritDoc
*/
protected const STATEMENT_CLASS = SqlserverStatement::class;
/**
* Base configuration settings for Sqlserver driver
*
* @var array<string, mixed>
*/
protected array $_baseConfig = [
'host' => 'localhost\SQLEXPRESS',
'username' => '',
'password' => '',
'database' => 'cake',
'port' => '',
// PDO::SQLSRV_ENCODING_UTF8
'encoding' => 65001,
'flags' => [],
'init' => [],
'settings' => [],
'attributes' => [],
'app' => null,
'connectionPooling' => null,
'failoverPartner' => null,
'loginTimeout' => null,
'multiSubnetFailover' => null,
'encrypt' => null,
'trustServerCertificate' => null,
'accessToken' => null,
'authentication' => null,
];
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '[';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = ']';
/**
* Establishes a connection to the database server.
*
* Please note that the PDO::ATTR_PERSISTENT attribute is not supported by
* the SQL Server PHP PDO drivers. As a result you cannot use the
* persistent config option when connecting to a SQL Server (for more
* information see: https://github.com/Microsoft/msphpsql/issues/65).
*
* @throws \InvalidArgumentException if an unsupported setting is in the driver config
* @return void
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$config = $this->_config;
if (isset($config['persistent']) && $config['persistent']) {
throw new InvalidArgumentException(
'Config setting "persistent" cannot be set to true, '
. 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT',
);
}
$config['flags'] += [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['encoding'])) {
$config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
}
$port = '';
if ($config['port']) {
$port = ',' . $config['port'];
}
$dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false";
if ($config['app'] !== null) {
$dsn .= ";APP={$config['app']}";
}
if ($config['connectionPooling'] !== null) {
$dsn .= ";ConnectionPooling={$config['connectionPooling']}";
}
if ($config['failoverPartner'] !== null) {
$dsn .= ";Failover_Partner={$config['failoverPartner']}";
}
if ($config['loginTimeout'] !== null) {
$dsn .= ";LoginTimeout={$config['loginTimeout']}";
}
if ($config['multiSubnetFailover'] !== null) {
$dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
}
if ($config['encrypt'] !== null) {
$dsn .= ";Encrypt={$config['encrypt']}";
}
if ($config['trustServerCertificate'] !== null) {
$dsn .= ";TrustServerCertificate={$config['trustServerCertificate']}";
}
if ($config['accessToken'] !== null) {
$dsn .= ";AccessToken={$config['accessToken']}";
}
if ($config['authentication'] !== null) {
$dsn .= ";Authentication={$config['authentication']}";
}
$this->pdo = $this->createPdo($dsn, $config);
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->pdo->exec($command);
}
}
if (!empty($config['settings']) && is_array($config['settings'])) {
foreach ($config['settings'] as $key => $value) {
$this->pdo->exec("SET {$key} {$value}");
}
}
if (!empty($config['attributes']) && is_array($config['attributes'])) {
foreach ($config['attributes'] as $key => $value) {
$this->pdo->setAttribute($key, $value);
}
}
}
/**
* Returns whether PHP is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('sqlsrv', PDO::getAvailableDrivers(), true);
}
/**
* @inheritDoc
*/
public function prepare(Query|string $query): StatementInterface
{
$options = [
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
];
$sql = $query;
if ($query instanceof Query) {
$sql = $query->sql();
if (count($query->getValueBinder()->bindings()) > 2100) {
throw new InvalidArgumentException(
'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' .
'This is probably due to a very large WHERE IN () clause which generates a parameter ' .
'for each value in the array. ' .
'If using an Association, try changing the `strategy` from select to subquery.',
);
}
if ($query instanceof SelectQuery && !$query->isBufferedResultsEnabled()) {
$options = [];
}
}
/** @var string $sql */
$statement = $this->getPdo()->prepare(
$sql,
$options,
);
/** @var \Cake\Database\StatementInterface */
return new (static::STATEMENT_CLASS)($statement, $this, $this->getResultSetDecorators($query));
}
/**
* @inheritDoc
*/
public function savePointSQL($name): string
{
return 'SAVE TRANSACTION t' . $name;
}
/**
* @inheritDoc
*/
public function releaseSavePointSQL($name): string
{
// SQLServer has no release save point operation.
return '';
}
/**
* @inheritDoc
*/
public function rollbackSavePointSQL($name): string
{
return 'ROLLBACK TRANSACTION t' . $name;
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
}
/**
* @inheritDoc
*/
public function supports(DriverFeatureEnum $feature): bool
{
return match ($feature) {
DriverFeatureEnum::CTE,
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
DriverFeatureEnum::SAVEPOINT,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
DriverFeatureEnum::WINDOW => true,
DriverFeatureEnum::INTERSECT => true,
DriverFeatureEnum::INTERSECT_ALL => false,
DriverFeatureEnum::JSON => false,
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
DriverFeatureEnum::CHECK_CONSTRAINTS => false,
};
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ??= new SqlserverSchemaDialect($this);
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\SqlserverCompiler
*/
public function newCompiler(): QueryCompiler
{
return new SqlserverCompiler();
}
/**
* @inheritDoc
*/
protected function _selectQueryTranslator(SelectQuery $query): SelectQuery
{
$limit = $query->clause('limit');
$offset = $query->clause('offset');
if ($limit && $offset === null) {
$query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
}
if ($offset !== null && !$query->clause('order')) {
$query->orderBy($query->expr()->add('(SELECT NULL)'));
}
if ($this->version() < 11 && $offset !== null) {
return $this->_pagingSubquery($query, $limit, $offset);
}
return $this->_transformDistinct($query);
}
/**
* Generate a paging subquery for older versions of SQLserver.
*
* Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
* be used.
*
* @param \Cake\Database\Query\SelectQuery<mixed> $original The query to wrap in a subquery.
* @param int|null $limit The number of rows to fetch.
* @param int|null $offset The number of rows to offset.
* @return \Cake\Database\Query\SelectQuery<mixed> Modified query object.
*/
protected function _pagingSubquery(SelectQuery $original, ?int $limit, ?int $offset): SelectQuery
{
$field = '_cake_paging_._cake_page_rownum_';
/** @var \Cake\Database\Expression\OrderByExpression $originalOrder */
$originalOrder = $original->clause('order');
if ($originalOrder) {
// SQL server does not support column aliases in OVER clauses. But
// the only practical way to specify the use of calculated columns
// is with their alias. So substitute the select SQL in place of
// any column aliases for those entries in the order clause.
$select = $original->clause('select');
$order = new OrderByExpression();
$originalOrder
->iterateParts(function ($direction, $orderBy) use ($select, $order) {
$key = $orderBy;
if (
isset($select[$orderBy]) &&
$select[$orderBy] instanceof ExpressionInterface
) {
$order->add(new OrderClauseExpression($select[$orderBy], $direction));
} else {
$order->add([$key => $direction]);
}
// Leave original order clause unchanged.
return $orderBy;
});
} else {
$order = new OrderByExpression('(SELECT NULL)');
}
$query = clone $original;
$query->select([
'_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order),
])->limit(null)
->offset(null)
->orderBy([], true);
$outer = $query->getConnection()->selectQuery();
$outer->select('*')
->from(['_cake_paging_' => $query]);
if ($offset) {
$outer->where(["{$field} > " . $offset]);
}
if ($limit) {
$value = (int)$offset + $limit;
$outer->where(["{$field} <= {$value}"]);
}
// Decorate the original query as that is what the
// end developer will be calling execute() on originally.
$original->decorateResults(function ($row) {
if (isset($row['_cake_page_rownum_'])) {
unset($row['_cake_page_rownum_']);
}
return $row;
});
return $outer;
}
/**
* @inheritDoc
*/
protected function _transformDistinct(SelectQuery $query): SelectQuery
{
if (!is_array($query->clause('distinct'))) {
return $query;
}
$original = $query;
$query = clone $original;
$distinct = $query->clause('distinct');
$query->distinct(false);
$order = new OrderByExpression($distinct);
$query
->select(function (Query $q) use ($distinct, $order) {
$over = $q->expr('ROW_NUMBER() OVER')
->add('(PARTITION BY')
->add($q->expr()->add($distinct)->setConjunction(','))
->add($order)
->add(')')
->setConjunction(' ');
return [
'_cake_distinct_pivot_' => $over,
];
})
->limit(null)
->offset(null)
->orderBy([], true);
$outer = new SelectQuery($query->getConnection());
$outer->select('*')
->from(['_cake_distinct_' => $query])
->where(['_cake_distinct_pivot_' => 1]);
// Decorate the original query as that is what the
// end developer will be calling execute() on originally.
$original->decorateResults(function ($row) {
if (isset($row['_cake_distinct_pivot_'])) {
unset($row['_cake_distinct_pivot_']);
}
return $row;
});
return $outer;
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
TupleComparison::class => '_transformTupleComparison',
];
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 + exp2
$expression->setName('')->setConjunction(' +');
break;
case 'DATEDIFF':
$hasDay = false;
$visitor = function ($value) use (&$hasDay) {
if ($value === 'day') {
$hasDay = true;
}
return $value;
};
$expression->iterateParts($visitor);
if (!$hasDay) {
$expression->add(['day' => 'literal'], [], true);
}
break;
case 'CURRENT_DATE':
$time = new FunctionExpression('GETUTCDATE');
$expression->setName('CONVERT')->add(['date' => 'literal', $time]);
break;
case 'CURRENT_TIME':
$time = new FunctionExpression('GETUTCDATE');
$expression->setName('CONVERT')->add(['time' => 'literal', $time]);
break;
case 'NOW':
$expression->setName('GETUTCDATE');
break;
case 'EXTRACT':
$expression->setName('DATEPART')->setConjunction(' ,');
break;
case 'DATE_ADD':
$params = [];
$visitor = function ($p, $key) use (&$params) {
if ($key === 0) {
$params[2] = $p;
} else {
$valueUnit = explode(' ', $p);
$params[0] = rtrim($valueUnit[1], 's');
$params[1] = $valueUnit[0];
}
return $p;
};
$manipulator = function ($p, $key) use (&$params) {
return $params[$key];
};
$expression
->setName('DATEADD')
->setConjunction(',')
->iterateParts($visitor)
->iterateParts($manipulator)
->add([$params[2] => 'literal']);
break;
case 'DAYOFWEEK':
$expression
->setName('DATEPART')
->setConjunction(' ')
->add(['weekday, ' => 'literal'], [], true);
break;
case 'SUBSTR':
$expression->setName('SUBSTRING');
if (count($expression) < 4) {
$params = [];
$expression
->iterateParts(function ($p) use (&$params) {
return $params[] = $p;
})
->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
}
break;
}
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Query;
use Cake\Database\Query\SelectQuery;
use InvalidArgumentException;
/**
* Provides a translator method for tuple comparisons
*
* @internal
*/
trait TupleComparisonTranslatorTrait
{
/**
* Receives a TupleExpression and changes it so that it conforms to this
* SQL dialect.
*
* It transforms expressions looking like '(a, b) IN ((c, d), (e, f))' into an
* equivalent expression of the form '((a = c) AND (b = d)) OR ((a = e) AND (b = f))'.
*
* It can also transform expressions where the right hand side is a query
* selecting the same amount of columns as the elements in the left hand side of
* the expression:
*
* (a, b) IN (SELECT c, d FROM a_table) is transformed into
*
* 1 = (SELECT 1 FROM a_table WHERE (a = c) AND (b = d))
*
* @param \Cake\Database\Expression\TupleComparison $expression The expression to transform
* @param \Cake\Database\Query $query The query to update.
* @return void
*/
protected function _transformTupleComparison(TupleComparison $expression, Query $query): void
{
$fields = $expression->getField();
if (!is_array($fields)) {
return;
}
$operator = strtoupper($expression->getOperator());
if (!in_array($operator, ['IN', '='])) {
throw new InvalidArgumentException(
sprintf(
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
$operator,
),
);
}
$value = $expression->getValue();
$true = new QueryExpression('1');
if ($value instanceof SelectQuery) {
/** @var array<string> $selected */
$selected = array_values($value->clause('select'));
foreach ($fields as $i => $field) {
$value->andWhere([$field => new IdentifierExpression($selected[$i])]);
}
$value->select($true, true);
$expression->setField($true);
$expression->setOperator('=');
return;
}
$type = $expression->getType();
if ($type) {
/** @var array<string, string> $typeMap */
$typeMap = array_combine($fields, $type) ?: [];
} else {
$typeMap = [];
}
$surrogate = $query->getConnection()
->selectQuery()
->select($true);
if (!is_array(current($value))) {
$value = [$value];
}
$conditions = ['OR' => []];
foreach ($value as $tuple) {
$item = [];
foreach (array_values($tuple) as $i => $value2) {
$item[] = [$fields[$i] => $value2];
}
$conditions['OR'][] = $item;
}
$surrogate->where($conditions, $typeMap);
$expression->setField($true);
$expression->setValue($surrogate);
$expression->setOperator('=');
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
enum DriverFeatureEnum: string
{
/**
* Common Table Expressions (with clause) support.
*/
case CTE = 'cte';
/**
* Disabling constraints without being in transaction support.
*/
case DISABLE_CONSTRAINT_WITHOUT_TRANSACTION = 'disable-constraint-without-transaction';
/**
* Native JSON data type support.
*/
case JSON = 'json';
/**
* Transaction savepoint support.
*/
case SAVEPOINT = 'savepoint';
/**
* Truncate with foreign keys attached support.
*/
case TRUNCATE_WITH_CONSTRAINTS = 'truncate-with-constraints';
/**
* Window function support (all or partial clauses).
*/
case WINDOW = 'window';
/**
* Intersect feature support
*/
case INTERSECT = 'intersect';
/**
* Intersect all feature support
*/
case INTERSECT_ALL = 'intersect-all';
/**
* Support for order by in set operations (union, intersect)
*/
case SET_OPERATIONS_ORDER_BY = 'set-operations-order-by';
/**
* Support for optimizer hints in comment form after statement keyword (SELECT <hint>, etc)
*/
case OPTIMIZER_HINT_COMMENT = 'optimizer-hint-comment';
/**
* Support for CHECK constraints.
*/
case CHECK_CONSTRAINTS = 'check-constraints';
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Exception for the database package.
*/
class DatabaseException extends CakeException
{
/**
* @inheritDoc
*/
protected string $_messageTemplate = '%s';
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingConnectionException
*/
class MissingConnectionException extends CakeException
{
/**
* @inheritDoc
*/
protected string $_messageTemplate = 'Connection to %s could not be established: %s';
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingDriverException
*/
class MissingDriverException extends CakeException
{
/**
* @inheritDoc
*/
protected string $_messageTemplate = 'Could not find driver `%s` for connection `%s`.';
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingExtensionException
*/
class MissingExtensionException extends CakeException
{
/**
* @inheritDoc
*/
// phpcs:ignore Generic.Files.LineLength
protected string $_messageTemplate = 'Database driver `%s` cannot be used due to a missing PHP extension or unmet dependency. Requested by connection `%s`';
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.4.3
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
use Throwable;
/**
* Class NestedTransactionRollbackException
*/
class NestedTransactionRollbackException extends CakeException
{
/**
* Constructor
*
* @param string|null $message If no message is given, a default message will be used.
* @param int|null $code Status code, defaults to 500.
* @param \Throwable|null $previous the previous exception.
*/
public function __construct(?string $message = null, ?int $code = 500, ?Throwable $previous = null)
{
$message ??= 'Cannot commit transaction - rollback() has been already called in the nested transaction';
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Database\Log\LoggedQuery;
use PDOException;
class QueryException extends PDOException
{
/**
* Constructor
*
* @param \Cake\Database\Log\LoggedQuery|string $query
* @param \PDOException $previous
*/
public function __construct(protected LoggedQuery|string $query, PDOException $previous)
{
$message = $previous->getMessage() . "\nQuery: " . $this->getQueryString();
parent::__construct($message, (int)$previous->getCode(), $previous);
}
/**
* Get the query string that caused this exception.
*
* @return string
*/
public function getQueryString(): string
{
if ($this->query instanceof LoggedQuery) {
return (string)$this->query;
}
return $this->query;
}
}
@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
use function Cake\Core\deprecationWarning;
/**
* This represents an SQL aggregate function expression in an SQL statement.
* Calls can be constructed by passing the name of the function and a list of params.
* For security reasons, all params passed are quoted by default unless
* explicitly told otherwise.
*/
class AggregateExpression extends FunctionExpression implements WindowInterface
{
/**
* @var \Cake\Database\Expression\QueryExpression|null
*/
protected ?QueryExpression $filter = null;
/**
* @var \Cake\Database\Expression\WindowExpression|null
*/
protected ?WindowExpression $window = null;
/**
* Adds conditions to the FILTER clause. The conditions are the same format as
* `Query::where()`.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The conditions to filter on.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @return $this
* @see \Cake\Database\Query::where()
*/
public function filter(ExpressionInterface|Closure|array|string $conditions, array $types = [])
{
$this->filter ??= new QueryExpression();
if ($conditions instanceof Closure) {
$conditions = $conditions(new QueryExpression());
}
$this->filter->add($conditions, $types);
return $this;
}
/**
* Adds an empty `OVER()` window expression or a named window expression.
*
* @param string|null $name Window name
* @return $this
*/
public function over(?string $name = null)
{
$window = $this->getWindow();
if ($name) {
// Set name manually in case this was chained from FunctionsBuilder wrapper
$window->name($name);
}
return $this;
}
/**
* @inheritDoc
*/
public function partition(ExpressionInterface|Closure|array|string $partitions)
{
$this->getWindow()->partition($partitions);
return $this;
}
/**
* @inheritDoc
*/
public function order(ExpressionInterface|Closure|array|string $fields)
{
deprecationWarning(
'5.0.0',
'AggregateExpression::order() is deprecated. Use AggregateExpression::orderBy() instead.',
);
return $this->orderBy($fields);
}
/**
* @inheritDoc
*/
public function orderBy(ExpressionInterface|Closure|array|string $fields)
{
$this->getWindow()->orderBy($fields);
return $this;
}
/**
* @inheritDoc
*/
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0)
{
$this->getWindow()->range($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function rows(?int $start, ?int $end = 0)
{
$this->getWindow()->rows($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function groups(?int $start, ?int $end = 0)
{
$this->getWindow()->groups($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function frame(
string $type,
ExpressionInterface|string|int|null $startOffset,
string $startDirection,
ExpressionInterface|string|int|null $endOffset,
string $endDirection,
) {
$this->getWindow()->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
return $this;
}
/**
* @inheritDoc
*/
public function excludeCurrent()
{
$this->getWindow()->excludeCurrent();
return $this;
}
/**
* @inheritDoc
*/
public function excludeGroup()
{
$this->getWindow()->excludeGroup();
return $this;
}
/**
* @inheritDoc
*/
public function excludeTies()
{
$this->getWindow()->excludeTies();
return $this;
}
/**
* Returns or creates WindowExpression for function.
*
* @return \Cake\Database\Expression\WindowExpression
*/
protected function getWindow(): WindowExpression
{
return $this->window ??= new WindowExpression();
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$sql = parent::sql($binder);
if ($this->filter !== null) {
$sql .= ' FILTER (WHERE ' . $this->filter->sql($binder) . ')';
}
if ($this->window !== null) {
if ($this->window->isNamedOnly()) {
$sql .= ' OVER ' . $this->window->sql($binder);
} else {
$sql .= ' OVER (' . $this->window->sql($binder) . ')';
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
parent::traverse($callback);
if ($this->filter !== null) {
$callback($this->filter);
$this->filter->traverse($callback);
}
if ($this->window !== null) {
$callback($this->window);
$this->window->traverse($callback);
}
return $this;
}
/**
* @inheritDoc
*/
public function count(): int
{
$count = parent::count();
if ($this->window !== null) {
$count += 1;
}
return $count;
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
parent::__clone();
if ($this->filter !== null) {
$this->filter = clone $this->filter;
}
if ($this->window !== null) {
$this->window = clone $this->window;
}
}
}
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object that represents a SQL BETWEEN snippet
*/
class BetweenExpression implements ExpressionInterface, FieldInterface
{
use ExpressionTypeCasterTrait;
use FieldTrait;
/**
* The first value in the expression
*
* @var mixed
*/
protected mixed $_from;
/**
* The second value in the expression
*
* @var mixed
*/
protected mixed $_to;
/**
* The data type for the from and to arguments
*
* @var mixed
*/
protected mixed $_type;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values in between the range.
* @param mixed $from The initial value of the range.
* @param mixed $to The ending value in the comparison range.
* @param string|null $type The data type name to bind the values with.
*/
public function __construct(ExpressionInterface|string $field, mixed $from, mixed $to, ?string $type = null)
{
if ($type !== null) {
$from = $this->_castToExpression($from, $type);
$to = $this->_castToExpression($to, $type);
}
$this->_field = $field;
$this->_from = $from;
$this->_to = $to;
$this->_type = $type;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$parts = [
'from' => $this->_from,
'to' => $this->_to,
];
$field = $this->_field;
if ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
foreach ($parts as $name => $part) {
if ($part instanceof ExpressionInterface) {
$parts[$name] = $part->sql($binder);
continue;
}
$parts[$name] = $this->_bindValue($part, $binder, $this->_type);
}
assert(is_string($field));
return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach ([$this->_field, $this->_from, $this->_to] as $part) {
if ($part instanceof ExpressionInterface) {
$callback($part);
}
}
return $this;
}
/**
* Registers a value in the placeholder generator and returns the generated placeholder
*
* @param mixed $value The value to bind
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* Do a deep clone of this expression.
*
* @return void
*/
public function __clone()
{
foreach (['_field', '_from', '_to'] as $part) {
if ($this->{$part} instanceof ExpressionInterface) {
$this->{$part} = clone $this->{$part};
}
}
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Chronos\ChronosDate;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypedResultInterface;
use Cake\Database\ValueBinder;
use DateTimeInterface;
use Stringable;
/**
* Trait that holds shared functionality for case related expressions.
*
* @internal
*/
trait CaseExpressionTrait
{
/**
* Infers the abstract type for the given value.
*
* @param mixed $value The value for which to infer the type.
* @return string|null The abstract type, or `null` if it could not be inferred.
*/
protected function inferType(mixed $value): ?string
{
$type = null;
if (is_string($value)) {
$type = 'string';
} elseif (is_int($value)) {
$type = 'integer';
} elseif (is_float($value)) {
$type = 'float';
} elseif (is_bool($value)) {
$type = 'boolean';
} elseif ($value instanceof ChronosDate) {
$type = 'date';
} elseif ($value instanceof DateTimeInterface) {
$type = 'datetime';
} elseif (
$value instanceof Stringable
) {
$type = 'string';
} elseif (
$this->_typeMap !== null &&
$value instanceof IdentifierExpression
) {
$type = $this->_typeMap->type($value->getIdentifier());
} elseif ($value instanceof TypedResultInterface) {
$type = $value->getReturnType();
}
return $type;
}
/**
* Compiles a nullable value to SQL.
*
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The value to compile.
* @param string|null $type The value type.
* @return string
*/
protected function compileNullableValue(ValueBinder $binder, mixed $value, ?string $type = null): string
{
if (
$type !== null &&
!($value instanceof ExpressionInterface)
) {
$value = $this->_castToExpression($value, $type);
}
if ($value === null) {
$value = 'NULL';
} elseif ($value instanceof Query) {
$value = sprintf('(%s)', $value->sql($binder));
} elseif ($value instanceof ExpressionInterface) {
$value = $value->sql($binder);
} else {
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
$value = $placeholder;
}
return $value;
}
}
@@ -0,0 +1,594 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypedResultInterface;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
use LogicException;
/**
* Represents a SQL case statement with a fluid API
*/
class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
{
use CaseExpressionTrait;
use ExpressionTypeCasterTrait;
use TypeMapTrait;
/**
* The names of the clauses that are valid for use with the
* `clause()` method.
*
* @var array<string>
*/
protected array $validClauseNames = [
'value',
'when',
'else',
];
/**
* Whether this is a simple case expression.
*
* @var bool
*/
protected bool $isSimpleVariant = false;
/**
* The case value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected mixed $value = null;
/**
* The case value type.
*
* @var string|null
*/
protected ?string $valueType = null;
/**
* The `WHEN ... THEN ...` expressions.
*
* @var array<\Cake\Database\Expression\WhenThenExpression>
*/
protected array $when = [];
/**
* Buffer that holds values and types for use with `then()`.
*
* @var array|null
*/
protected ?array $whenBuffer = null;
/**
* The else part result value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected mixed $else = null;
/**
* The else part result type.
*
* @var string|null
*/
protected ?string $elseType = null;
/**
* The return type.
*
* @var string|null
*/
protected ?string $returnType = null;
/**
* Constructor.
*
* When a value is set, the syntax generated is
* `CASE case_value WHEN when_value ... END` (simple case),
* where the `when_value`'s are compared against the
* `case_value`.
*
* When no value is set, the syntax generated is
* `CASE WHEN when_conditions ... END` (searched case),
* where the conditions hold the comparisons.
*
* Note that `null` is a valid case value, and thus should
* only be passed if you actually want to create the simple
* case expression variant!
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
* from the value.
*/
public function __construct(mixed $value = null, ?string $type = null)
{
if (func_num_args() > 0) {
if (
$value !== null &&
!is_scalar($value) &&
!(is_object($value) && !($value instanceof Closure))
) {
throw new InvalidArgumentException(sprintf(
'The `$value` argument must be either `null`, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
get_debug_type($value),
));
}
$this->value = $value;
if (
$value !== null &&
$type === null &&
!($value instanceof ExpressionInterface)
) {
$type = $this->inferType($value);
}
$this->valueType = $type;
$this->isSimpleVariant = true;
}
}
/**
* Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a
* self-contained expression that holds both the value for `WHEN`
* and the value for `THEN`.
*
* ### Order based syntax
*
* When passing a value other than a self-contained
* `\Cake\Database\Expression\WhenThenExpression`,
* instance, the `WHEN ... THEN ...` statement must be closed off with
* a call to `then()` before invoking `when()` again or `else()`:
*
* ```
* $queryExpression
* ->case($query->identifier('Table.column'))
* ->when(true)
* ->then('Yes')
* ->when(false)
* ->then('No')
* ->else('Maybe');
* ```
*
* ### Self-contained expressions
*
* When passing an instance of `\Cake\Database\Expression\WhenThenExpression`,
* being it directly, or via a callable, then there is no need to close
* using `then()` on this object, instead the statement will be closed
* on the `\Cake\Database\Expression\WhenThenExpression`
* object using
* `\Cake\Database\Expression\WhenThenExpression::then()`.
*
* Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`,
* and must return one, being it the same object, or a custom one:
*
* ```
* $queryExpression
* ->case()
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['Table.column' => true])
* ->then('Yes');
* })
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['Table.column' => false])
* ->then('No');
* })
* ->else('Maybe');
* ```
*
* ### Type handling
*
* The types provided via the `$type` argument will be merged with the
* type map set for this expression. When using callables for `$when`,
* the `\Cake\Database\Expression\WhenThenExpression`
* instance received by the callables will inherit that type map, however
* the types passed here will _not_ be merged in case of using callables,
* instead the types must be passed in
* `\Cake\Database\Expression\WhenThenExpression::when()`:
*
* ```
* $queryExpression
* ->case()
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['unmapped_column' => true], ['unmapped_column' => 'bool'])
* ->then('Yes');
* })
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['unmapped_column' => false], ['unmapped_column' => 'bool'])
* ->then('No');
* })
* ->else('Maybe');
* ```
*
* ### User data safety
*
* When passing user data, be aware that allowing a user defined array
* to be passed, is a potential SQL injection vulnerability, as it
* allows for raw SQL to slip in!
*
* The following is _unsafe_ usage that must be avoided:
*
* ```
* $case
* ->when($userData)
* ```
*
* A safe variant for the above would be to define a single type for
* the value:
*
* ```
* $case
* ->when($userData, 'integer')
* ```
*
* This way an exception would be triggered when an array is passed for
* the value, thus preventing raw SQL from slipping in, and all other
* types of values would be forced to be bound as an integer.
*
* Another way to safely pass user data is when using a conditions
* array, and passing user data only on the value side of the array
* entries, which will cause them to be bound:
*
* ```
* $case
* ->when([
* 'Table.column' => $userData,
* ])
* ```
*
* Lastly, data can also be bound manually:
*
* ```
* $query
* ->select([
* 'val' => $query->expr()
* ->case()
* ->when($query->expr(':userData'))
* ->then(123)
* ])
* ->bind(':userData', $userData, 'integer')
* ```
*
* @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an
* array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is
* _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If
* you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to
* be a non-array, and then always binds the data), use a conditions array where the user data is only passed on
* the value side of the array entries, or custom bindings!
* @param array<string, string>|string|null $type The when value type. Either an associative array when using array style
* conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value.
* @return $this
* @throws \LogicException In case this a closing `then()` call is required before calling this method.
* @throws \LogicException In case the callable doesn't return an instance of
* `\Cake\Database\Expression\WhenThenExpression`.
*/
public function when(mixed $when, array|string|null $type = null)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Cannot call `when()` between `when()` and `then()`.');
}
if ($when instanceof Closure) {
$when = $when(new WhenThenExpression($this->getTypeMap()));
if (!($when instanceof WhenThenExpression)) {
throw new LogicException(sprintf(
'`when()` callables must return an instance of `\%s`, `%s` given.',
WhenThenExpression::class,
get_debug_type($when),
));
}
}
if ($when instanceof WhenThenExpression) {
$this->when[] = $when;
} else {
$this->whenBuffer = ['when' => $when, 'type' => $type];
}
return $this;
}
/**
* Sets the `THEN` result value for the last `WHEN ... THEN ...`
* statement that was opened using `when()`.
*
* ### Order based syntax
*
* This method can only be invoked in case `when()` was previously
* used with a value other than a closure or an instance of
* `\Cake\Database\Expression\WhenThenExpression`:
*
* ```
* $case
* ->when(['Table.column' => true])
* ->then('Yes')
* ->when(['Table.column' => false])
* ->then('No')
* ->else('Maybe');
* ```
*
* The following would all fail with an exception:
*
* ```
* $case
* ->when(['Table.column' => true])
* ->when(['Table.column' => false])
* // ...
* ```
*
* ```
* $case
* ->when(['Table.column' => true])
* ->else('Maybe')
* // ...
* ```
*
* ```
* $case
* ->then('Yes')
* // ...
* ```
*
* ```
* $case
* ->when(['Table.column' => true])
* ->then('Yes')
* ->then('No')
* // ...
* ```
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
* value.
* @return $this
* @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an
* instance of `\Cake\Database\Expression\WhenThenExpression`.
*/
public function then(mixed $result, ?string $type = null)
{
if ($this->whenBuffer === null) {
throw new LogicException('Cannot call `then()` before `when()`.');
}
$whenThen = (new WhenThenExpression($this->getTypeMap()))
->when($this->whenBuffer['when'], $this->whenBuffer['type'])
->then($result, $type);
$this->whenBuffer = null;
$this->when[] = $whenThen;
return $this;
}
/**
* Sets the `ELSE` result value.
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
* value.
* @return $this
* @throws \LogicException In case a closing `then()` call is required before calling this method.
* @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an
* instance of `\Cake\Database\ExpressionInterface`, or `null`.
*/
public function else(mixed $result, ?string $type = null)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Cannot call `else()` between `when()` and `then()`.');
}
if (
$result !== null &&
!is_scalar($result) &&
!(is_object($result) && !($result instanceof Closure))
) {
throw new InvalidArgumentException(sprintf(
'The `$result` argument must be either `null`, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
get_debug_type($result),
));
}
$type ??= $this->inferType($result);
$this->else = $result;
$this->elseType = $type;
return $this;
}
/**
* Returns the abstract type that this expression will return.
*
* If no type has been explicitly set via `setReturnType()`, this
* method will try to obtain the type from the result types of the
* `then()` and `else() `calls. All types must be identical in order
* for this to work, otherwise the type will default to `string`.
*
* @return string
* @see CaseStatementExpression::then()
*/
public function getReturnType(): string
{
if ($this->returnType !== null) {
return $this->returnType;
}
$types = [];
foreach ($this->when as $when) {
$type = $when->getResultType();
if ($type !== null) {
$types[] = $type;
}
}
if ($this->elseType !== null) {
$types[] = $this->elseType;
}
$types = array_unique($types);
if (count($types) === 1) {
return $types[0];
}
return 'string';
}
/**
* Sets the abstract type that this expression will return.
*
* If no type is being explicitly set via this method, then the
* `getReturnType()` method will try to infer the type from the
* result types of the `then()` and `else() `calls.
*
* @param string $type The type name to use.
* @return $this
*/
public function setReturnType(string $type)
{
$this->returnType = $type;
return $this;
}
/**
* Returns the available data for the given clause.
*
* ### Available clauses
*
* The following clause names are available:
*
* * `value`: The case value for a `CASE case_value WHEN ...` expression.
* * `when`: An array of `WHEN ... THEN ...` expressions.
* * `else`: The `ELSE` result value.
*
* @param string $clause The name of the clause to obtain.
* @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null
* @throws \InvalidArgumentException In case the given clause name is invalid.
*/
public function clause(string $clause): mixed
{
if (!in_array($clause, $this->validClauseNames, true)) {
throw new InvalidArgumentException(
sprintf(
'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.',
implode('`, `', $this->validClauseNames),
$clause,
),
);
}
return $this->{$clause};
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if (!$this->when) {
throw new LogicException('Case expression must have at least one when statement.');
}
$value = '';
if ($this->isSimpleVariant) {
$value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' ';
}
$whenThenExpressions = [];
foreach ($this->when as $whenThen) {
$whenThenExpressions[] = $whenThen->sql($binder);
}
$whenThen = implode(' ', $whenThenExpressions);
$else = $this->compileNullableValue($binder, $this->else, $this->elseType);
return "CASE {$value}{$whenThen} ELSE {$else} END";
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if ($this->value instanceof ExpressionInterface) {
$callback($this->value);
$this->value->traverse($callback);
}
foreach ($this->when as $when) {
$callback($when);
$when->traverse($callback);
}
if ($this->else instanceof ExpressionInterface) {
$callback($this->else);
$this->else->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if ($this->value instanceof ExpressionInterface) {
$this->value = clone $this->value;
}
foreach ($this->when as $key => $when) {
$this->when[$key] = clone $this->when[$key];
}
if ($this->else instanceof ExpressionInterface) {
$this->else = clone $this->else;
}
}
}
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression that represents a common table expression definition.
*/
class CommonTableExpression implements ExpressionInterface
{
/**
* The CTE name.
*
* @var \Cake\Database\Expression\IdentifierExpression
*/
protected IdentifierExpression $name;
/**
* The field names to use for the CTE.
*
* @var array<\Cake\Database\Expression\IdentifierExpression>
*/
protected array $fields = [];
/**
* The CTE query definition.
*
* @var \Cake\Database\ExpressionInterface|null
*/
protected ?ExpressionInterface $query = null;
/**
* Whether the CTE is materialized or not materialized.
*
* @var string|null
*/
protected ?string $materialized = null;
/**
* Whether the CTE is recursive.
*
* @var bool
*/
protected bool $recursive = false;
/**
* Constructor.
*
* @param string $name The CTE name.
* @param \Cake\Database\ExpressionInterface|\Closure|null $query CTE query
*/
public function __construct(string $name = '', ExpressionInterface|Closure|null $query = null)
{
$this->name = new IdentifierExpression($name);
if ($query) {
$this->query($query);
}
}
/**
* Sets the name of this CTE.
*
* This is the named you used to reference the expression
* in select, insert, etc queries.
*
* @param string $name The CTE name.
* @return $this
*/
public function name(string $name)
{
$this->name = new IdentifierExpression($name);
return $this;
}
/**
* Sets the query for this CTE.
*
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
* @return $this
*/
public function query(ExpressionInterface|Closure $query)
{
if ($query instanceof Closure) {
$query = $query();
if (!($query instanceof ExpressionInterface)) {
throw new DatabaseException(
'You must return an `ExpressionInterface` from a Closure passed to `query()`.',
);
}
}
$this->query = $query;
return $this;
}
/**
* Adds one or more fields (arguments) to the CTE.
*
* @param \Cake\Database\Expression\IdentifierExpression|array<string>|array<\Cake\Database\Expression\IdentifierExpression>|string $fields Field names
* @return $this
*/
public function field(IdentifierExpression|array|string $fields)
{
$fields = (array)$fields;
/** @var array<string|\Cake\Database\Expression\IdentifierExpression> $fields */
foreach ($fields as &$field) {
if (!($field instanceof IdentifierExpression)) {
$field = new IdentifierExpression($field);
}
}
/** @var array<\Cake\Database\Expression\IdentifierExpression> $mergedFields */
$mergedFields = array_merge($this->fields, $fields);
$this->fields = $mergedFields;
return $this;
}
/**
* Sets this CTE as materialized.
*
* @return $this
*/
public function materialized()
{
$this->materialized = 'MATERIALIZED';
return $this;
}
/**
* Sets this CTE as not materialized.
*
* @return $this
*/
public function notMaterialized()
{
$this->materialized = 'NOT MATERIALIZED';
return $this;
}
/**
* Gets whether this CTE is recursive.
*
* @return bool
*/
public function isRecursive(): bool
{
return $this->recursive;
}
/**
* Sets this CTE as recursive.
*
* @return $this
*/
public function recursive()
{
$this->recursive = true;
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$fields = '';
if ($this->fields) {
$expressions = array_map(fn(IdentifierExpression $e) => $e->sql($binder), $this->fields);
$fields = sprintf('(%s)', implode(', ', $expressions));
}
$suffix = $this->materialized ? $this->materialized . ' ' : '';
return sprintf(
'%s%s AS %s(%s)',
$this->name->sql($binder),
$fields,
$suffix,
$this->query ? $this->query->sql($binder) : '',
);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
$callback($this->name);
foreach ($this->fields as $field) {
$callback($field);
$field->traverse($callback);
}
if ($this->query) {
$callback($this->query);
$this->query->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
$this->name = clone $this->name;
if ($this->query) {
$this->query = clone $this->query;
}
foreach ($this->fields as $key => $field) {
$this->fields[$key] = clone $field;
}
}
}
@@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* A Comparison is a type of query expression that represents an operation
* involving a field an operator and a value. In its most common form the
* string representation of a comparison is `field = value`
*/
class ComparisonExpression implements ExpressionInterface, FieldInterface
{
use ExpressionTypeCasterTrait;
use FieldTrait;
/**
* The value to be used in the right hand side of the operation
*
* @var mixed
*/
protected mixed $_value;
/**
* The type to be used for casting the value to a database representation
*
* @var string|null
*/
protected ?string $_type = null;
/**
* The operator used for comparing field and value
*
* @var string
*/
protected string $_operator = '=';
/**
* Whether the value in this expression is a traversable
*
* @var bool
*/
protected bool $_isMultiple = false;
/**
* A cached list of ExpressionInterface objects that were
* found in the value for this expression.
*
* @var array<\Cake\Database\ExpressionInterface>
*/
protected array $_valueExpressions = [];
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field the field name to compare to a value
* @param mixed $value The value to be used in comparison
* @param string|null $type the type name used to cast the value
* @param string $operator the operator used for comparing field and value
*/
public function __construct(
ExpressionInterface|string $field,
mixed $value,
?string $type = null,
string $operator = '=',
) {
$this->_type = $type;
$this->setField($field);
$this->setValue($value);
$this->_operator = $operator;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue(mixed $value): void
{
$value = $this->_castToExpression($value, $this->_type);
$isMultiple = $this->_type && str_contains($this->_type, '[]');
if ($isMultiple) {
[$value, $this->_valueExpressions] = $this->_collectExpressions($value);
}
$this->_isMultiple = $isMultiple;
$this->_value = $value;
}
/**
* Returns the value used for comparison
*
* @return mixed
*/
public function getValue(): mixed
{
return $this->_value;
}
/**
* Sets the operator to use for the comparison
*
* @param string $operator The operator to be used for the comparison.
* @return void
*/
public function setOperator(string $operator): void
{
$this->_operator = $operator;
}
/**
* Returns the operator used for comparison
*
* @return string
*/
public function getOperator(): string
{
return $this->_operator;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$field = $this->_field;
if ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
if ($this->_value instanceof IdentifierExpression) {
$template = '%s %s %s';
$value = $this->_value->sql($binder);
} elseif ($this->_value instanceof ExpressionInterface) {
$template = '%s %s (%s)';
$value = $this->_value->sql($binder);
} else {
[$template, $value] = $this->_stringExpression($binder);
}
assert(is_string($field));
return sprintf($template, $field, $this->_operator, $value);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_field instanceof ExpressionInterface) {
$callback($this->_field);
$this->_field->traverse($callback);
}
if ($this->_value instanceof ExpressionInterface) {
$callback($this->_value);
$this->_value->traverse($callback);
}
foreach ($this->_valueExpressions as $v) {
$callback($v);
$v->traverse($callback);
}
return $this;
}
/**
* Create a deep clone.
*
* Clones the field and value if they are expression objects.
*
* @return void
*/
public function __clone()
{
foreach (['_value', '_field'] as $prop) {
if ($this->{$prop} instanceof ExpressionInterface) {
$this->{$prop} = clone $this->{$prop};
}
}
}
/**
* Returns a template and a placeholder for the value after registering it
* with the placeholder $binder
*
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @return array First position containing the template and the second a placeholder
*/
protected function _stringExpression(ValueBinder $binder): array
{
$template = '%s ';
if ($this->_field instanceof ExpressionInterface && !$this->_field instanceof IdentifierExpression) {
$template = '(%s) ';
}
if ($this->_isMultiple) {
$template .= '%s (%s)';
$type = $this->_type;
if ($type !== null) {
$type = str_replace('[]', '', $type);
}
$value = $this->_flattenValue($this->_value, $binder, $type);
// To avoid SQL errors when comparing a field to a list of empty values,
// better just throw an exception here
if ($value === '') {
$field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field;
/** @var string $field */
throw new DatabaseException(
"Impossible to generate condition with empty list of values for field ({$field})",
);
}
} else {
$template .= '%s %s';
$value = $this->_bindValue($this->_value, $binder, $this->_type);
}
return [$template, $value];
}
/**
* Registers a value in the placeholder generator and returns the generated placeholder
*
* @param mixed $value The value to bind
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* Converts a traversable value into a set of placeholders generated by
* $binder and separated by `,`
*
* @param iterable $value the value to flatten
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type the type to cast values to
* @return string
*/
protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string
{
$parts = [];
if (is_array($value)) {
foreach ($this->_valueExpressions as $k => $v) {
$parts[$k] = $v->sql($binder);
unset($value[$k]);
}
}
if ($value) {
$parts += $binder->generateManyNamed($value, $type);
}
return implode(',', $parts);
}
/**
* Returns an array with the original $values in the first position
* and all ExpressionInterface objects that could be found in the second
* position.
*
* @param \Cake\Database\ExpressionInterface|iterable $values The rows to insert
* @return array
*/
protected function _collectExpressions(ExpressionInterface|iterable $values): array
{
if ($values instanceof ExpressionInterface) {
return [$values, []];
}
$expressions = [];
$result = [];
$isArray = is_array($values);
if ($isArray) {
$result = (array)$values;
}
foreach ($values as $k => $v) {
if ($v instanceof ExpressionInterface) {
$expressions[$k] = $v;
}
if ($isArray) {
$result[$k] = $v;
}
}
return [$result, $expressions];
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
/**
* Describes a getter and a setter for the a field property. Useful for expressions
* that contain an identifier to compare against.
*/
interface FieldInterface
{
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField(ExpressionInterface|array|string $field): void;
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField(): ExpressionInterface|array|string;
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
/**
* Contains the field property with a getter and a setter for it
*/
trait FieldTrait
{
/**
* The field name or expression to be used in the left hand side of the operator
*
* @var \Cake\Database\ExpressionInterface|array|string
*/
protected ExpressionInterface|array|string $_field;
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField(ExpressionInterface|array|string $field): void
{
$this->_field = $field;
}
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField(): ExpressionInterface|array|string
{
return $this->_field;
}
}
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypedResultInterface;
use Cake\Database\TypedResultTrait;
use Cake\Database\ValueBinder;
/**
* This class represents a function call string in a SQL statement. Calls can be
* constructed by passing the name of the function and a list of params.
* For security reasons, all params passed are quoted by default unless
* explicitly told otherwise.
*/
class FunctionExpression extends QueryExpression implements TypedResultInterface
{
use ExpressionTypeCasterTrait;
use TypedResultTrait;
/**
* The name of the function to be constructed when generating the SQL string
*
* @var string
*/
protected string $_name;
/**
* Constructor. Takes a name for the function to be invoked and a list of params
* to be passed into the function. Optionally you can pass a list of types to
* be used for each bound param.
*
* By default, all params that are passed will be quoted. If you wish to use
* literal arguments, you need to explicitly hint this function.
*
* ### Examples:
*
* `$f = new FunctionExpression('CONCAT', ['CakePHP', ' rules']);`
*
* Previous line will generate `CONCAT('CakePHP', ' rules')`
*
* `$f = new FunctionExpression('CONCAT', ['name' => 'literal', ' rules']);`
*
* Will produce `CONCAT(name, ' rules')`
*
* @param string $name the name of the function to be constructed
* @param array $params list of arguments to be passed to the function
* If associative the key would be used as argument when value is 'literal'
* @param array<string, string>|array<string|null> $types Associative array of types to be associated with the
* passed arguments
* @param string $returnType The return type of this expression
*/
public function __construct(string $name, array $params = [], array $types = [], string $returnType = 'string')
{
$this->_name = $name;
$this->_returnType = $returnType;
parent::__construct($params, $types, ',');
}
/**
* Sets the name of the SQL function to be invoke in this expression.
*
* @param string $name The name of the function
* @return $this
*/
public function setName(string $name)
{
$this->_name = $name;
return $this;
}
/**
* Gets the name of the SQL function to be invoke in this expression.
*
* @return string
*/
public function getName(): string
{
return $this->_name;
}
/**
* Adds one or more arguments for the function call.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions list of arguments to be passed to the function
* If associative the key would be used as argument when value is 'literal'
* @param array<string, string> $types Associative array of types to be associated with the
* passed arguments
* @param bool $prepend Whether to prepend or append to the list of arguments
* @see \Cake\Database\Expression\FunctionExpression::__construct() for more details.
* @return $this
*/
public function add(ExpressionInterface|array|string $conditions, array $types = [], bool $prepend = false)
{
$put = $prepend ? 'array_unshift' : 'array_push';
$typeMap = $this->getTypeMap()->setTypes($types);
/** @var array $conditions */
foreach ($conditions as $k => $p) {
if ($p === 'literal') {
$put($this->_conditions, $k);
continue;
}
if ($p === 'identifier') {
$put($this->_conditions, new IdentifierExpression($k));
continue;
}
$type = $typeMap->type($k);
if ($type !== null && !$p instanceof ExpressionInterface) {
$p = $this->_castToExpression($p, $type);
}
if ($p instanceof ExpressionInterface) {
$put($this->_conditions, $p);
continue;
}
$put($this->_conditions, ['value' => $p, 'type' => $type]);
}
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$parts = [];
foreach ($this->_conditions as $condition) {
if ($condition instanceof Query) {
$condition = sprintf('(%s)', $condition->sql($binder));
} elseif ($condition instanceof ExpressionInterface) {
$condition = $condition->sql($binder);
} elseif (is_array($condition)) {
$p = $binder->placeholder('param');
$binder->bind($p, $condition['value'], $condition['type']);
$condition = $p;
}
$parts[] = $condition;
}
return $this->_name . sprintf('(%s)', implode(
$this->_conjunction . ' ',
$parts,
));
}
/**
* The name of the function is in itself an expression to generate, thus
* always adding 1 to the amount of expressions stored in this object.
*
* @return int
*/
public function count(): int
{
return 1 + count($this->_conditions);
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* Represents a single identifier name in the database.
*
* Identifier values are unsafe with user supplied data.
* Values will be quoted when identifier quoting is enabled.
*
* @see \Cake\Database\Query::identifier()
*/
class IdentifierExpression implements ExpressionInterface
{
/**
* Holds the identifier string
*
* @var string
*/
protected string $_identifier;
/**
* @var string|null
*/
protected ?string $collation = null;
/**
* Constructor
*
* @param string $identifier The identifier this expression represents
* @param string|null $collation The identifier collation
*/
public function __construct(string $identifier, ?string $collation = null)
{
$this->_identifier = $identifier;
$this->collation = $collation;
}
/**
* Sets the identifier this expression represents
*
* @param string $identifier The identifier
* @return void
*/
public function setIdentifier(string $identifier): void
{
$this->_identifier = $identifier;
}
/**
* Returns the identifier this expression represents
*
* @return string
*/
public function getIdentifier(): string
{
return $this->_identifier;
}
/**
* Sets the collation.
*
* @param string $collation Identifier collation
* @return void
*/
public function setCollation(string $collation): void
{
$this->collation = $collation;
}
/**
* Returns the collation.
*
* @return string|null
*/
public function getCollation(): ?string
{
return $this->collation;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$sql = $this->_identifier;
if ($this->collation) {
$sql .= ' COLLATE ' . $this->collation;
}
return $sql;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
return $this;
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\TypeMap;
use Cake\Database\ValueBinder;
use InvalidArgumentException;
/**
* An expression object for ORDER BY clauses
*/
class OrderByExpression extends QueryExpression
{
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions The sort columns
* @param \Cake\Database\TypeMap|array<string, string> $types The types for each column.
* @param string $conjunction The glue used to join conditions together.
*/
public function __construct(
ExpressionInterface|array|string $conditions = [],
TypeMap|array $types = [],
string $conjunction = '',
) {
parent::__construct($conditions, $types, $conjunction);
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$order = [];
foreach ($this->_conditions as $k => $direction) {
if ($direction instanceof ExpressionInterface) {
$direction = $direction->sql($binder);
}
$order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction);
}
return sprintf('ORDER BY %s', implode(', ', $order));
}
/**
* Auxiliary function used for decomposing a nested array of conditions and
* building a tree structure inside this object to represent the full SQL expression.
*
* New order by expressions are merged to existing ones
*
* @param array $conditions list of order by expressions
* @param array $types list of types associated on fields referenced in $conditions
* @return void
*/
protected function _addConditions(array $conditions, array $types): void
{
foreach ($conditions as $key => $val) {
if (
is_string($key) &&
is_string($val) &&
!in_array(strtoupper($val), ['ASC', 'DESC'], true)
) {
throw new InvalidArgumentException(
sprintf(
"Passing extra expressions by associative array (`'%s' => '%s'`) " .
'is not allowed to avoid potential SQL injection. ' .
'Use QueryExpression or numeric array instead.',
$key,
$val,
),
);
}
}
$this->_conditions = array_merge($this->_conditions, $conditions);
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object for complex ORDER BY clauses
*/
class OrderClauseExpression implements ExpressionInterface, FieldInterface
{
use FieldTrait;
/**
* The direction of sorting.
*
* @var string
*/
protected string $_direction;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field to order on.
* @param string $direction The direction to sort on.
*/
public function __construct(ExpressionInterface|string $field, string $direction)
{
$this->_field = $field;
$this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$field = $this->_field;
if ($field instanceof Query) {
$field = sprintf('(%s)', $field->sql($binder));
} elseif ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
assert(is_string($field));
return sprintf('%s %s', $field, $this->_direction);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_field instanceof ExpressionInterface) {
$callback($this->_field);
$this->_field->traverse($callback);
}
return $this;
}
/**
* Create a deep clone of the order clause.
*
* @return void
*/
public function __clone()
{
if ($this->_field instanceof ExpressionInterface) {
$this->_field = clone $this->_field;
}
}
}
@@ -0,0 +1,788 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypeMap;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use Countable;
use InvalidArgumentException;
/**
* Represents a SQL Query expression. Internally it stores a tree of
* expressions that can be compiled by converting this object to string
* and will contain a correctly parenthesized and nested expression.
*/
class QueryExpression implements ExpressionInterface, Countable
{
use TypeMapTrait;
/**
* String to be used for joining each of the internal expressions
* this object internally stores for example "AND", "OR", etc.
*
* @var string
*/
protected string $_conjunction;
/**
* A list of strings or other expression objects that represent the "branches" of
* the expression tree. For example one key of the array might look like "sum > :value"
*
* @var array
*/
protected array $_conditions = [];
/**
* Constructor. A new expression object can be created without any params and
* be built dynamically. Otherwise, it is possible to pass an array of conditions
* containing either a tree-like array structure to be parsed and/or other
* expression objects. Optionally, you can set the conjunction keyword to be used
* for joining each part of this level of the expression tree.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions Tree like array structure
* containing all the conditions to be added or nested inside this expression object.
* @param \Cake\Database\TypeMap|array $types Associative array of types to be associated with the values
* passed in $conditions.
* @param string $conjunction the glue that will join all the string conditions at this
* level of the expression tree. For example "AND", "OR", "XOR"...
* @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types
*/
public function __construct(
ExpressionInterface|array|string $conditions = [],
TypeMap|array $types = [],
string $conjunction = 'AND',
) {
$this->setTypeMap($types);
$this->setConjunction(strtoupper($conjunction));
if ($conditions) {
$this->add($conditions, $this->getTypeMap()->getTypes());
}
}
/**
* Changes the conjunction for the conditions at this level of the expression tree.
*
* @param string $conjunction Value to be used for joining conditions
* @return $this
*/
public function setConjunction(string $conjunction)
{
$this->_conjunction = strtoupper($conjunction);
return $this;
}
/**
* Gets the currently configured conjunction for the conditions at this level of the expression tree.
*
* @return string
*/
public function getConjunction(): string
{
return $this->_conjunction;
}
/**
* Adds one or more conditions to this expression object. Conditions can be
* expressed in a one dimensional array, that will cause all conditions to
* be added directly at this level of the tree or they can be nested arbitrarily
* making it create more expression objects that will be nested inside and
* configured to use the specified conjunction.
*
* If the type passed for any of the fields is expressed "type[]" (note braces)
* then it will cause the placeholder to be re-written dynamically so if the
* value is an array, it will create as many placeholders as values are in it.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions single or multiple conditions to
* be added. When using an array and the key is 'OR' or 'AND' a new expression
* object will be created with that conjunction and internal array value passed
* as conditions.
* @param array<int|string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @see \Cake\Database\Query::where() for examples on conditions
* @return $this
*/
public function add(ExpressionInterface|array|string $conditions, array $types = [])
{
if (is_string($conditions) || $conditions instanceof ExpressionInterface) {
$this->_conditions[] = $conditions;
return $this;
}
$this->_addConditions($conditions, $types);
return $this;
}
/**
* Adds a new condition to the expression object in the form "field = value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* If it is suffixed with "[]" and the value is an array then multiple placeholders
* will be created, one per each value in the array.
* @return $this
*/
public function eq(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, '='));
}
/**
* Adds a new condition to the expression object in the form "field != value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* If it is suffixed with "[]" and the value is an array then multiple placeholders
* will be created, one per each value in the array.
* @return $this
*/
public function notEq(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, '!='));
}
/**
* Adds a new condition to the expression object in the form "field > value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function gt(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, '>'));
}
/**
* Adds a new condition to the expression object in the form "field < value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function lt(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, '<'));
}
/**
* Adds a new condition to the expression object in the form "field >= value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function gte(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, '>='));
}
/**
* Adds a new condition to the expression object in the form "field <= value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function lte(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, '<='));
}
/**
* Adds a new condition to the expression object in the form "field IS NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field database field to be
* tested for null
* @return $this
*/
public function isNull(ExpressionInterface|string $field)
{
if (!($field instanceof ExpressionInterface)) {
$field = new IdentifierExpression($field);
}
return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX));
}
/**
* Adds a new condition to the expression object in the form "field IS NOT NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field database field to be
* tested for not null
* @return $this
*/
public function isNotNull(ExpressionInterface|string $field)
{
if (!($field instanceof ExpressionInterface)) {
$field = new IdentifierExpression($field);
}
return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX));
}
/**
* Adds a new condition to the expression object in the form "field LIKE value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function like(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, 'LIKE'));
}
/**
* Adds a new condition to the expression object in the form "field NOT LIKE value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notLike(ExpressionInterface|string $field, mixed $value, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new ComparisonExpression($field, $value, $type, 'NOT LIKE'));
}
/**
* Adds a new condition to the expression object in the form
* "field IN (value1, value2)".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function in(
ExpressionInterface|string $field,
ExpressionInterface|array|string $values,
?string $type = null,
) {
$type ??= $this->_calculateType($field);
$type = $type ?: 'string';
$type .= '[]';
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
return $this->add(new ComparisonExpression($field, $values, $type, 'IN'));
}
/**
* Returns a new case expression object.
*
* When a value is set, the syntax generated is
* `CASE case_value WHEN when_value ... END` (simple case),
* where the `when_value`'s are compared against the
* `case_value`.
*
* When no value is set, the syntax generated is
* `CASE WHEN when_conditions ... END` (searched case),
* where the conditions hold the comparisons.
*
* Note that `null` is a valid case value, and thus should
* only be passed if you actually want to create the simple
* case expression variant!
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
* from the value.
* @return \Cake\Database\Expression\CaseStatementExpression
*/
public function case(mixed $value = null, ?string $type = null): CaseStatementExpression
{
if (func_num_args() > 0) {
$expression = new CaseStatementExpression($value, $type);
} else {
$expression = new CaseStatementExpression();
}
return $expression->setTypeMap($this->getTypeMap());
}
/**
* Adds a new condition to the expression object in the form
* "field NOT IN (value1, value2)".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notIn(
ExpressionInterface|string $field,
ExpressionInterface|array|string $values,
?string $type = null,
) {
$type ??= $this->_calculateType($field);
$type = $type ?: 'string';
$type .= '[]';
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
return $this->add(new ComparisonExpression($field, $values, $type, 'NOT IN'));
}
/**
* Adds a new condition to the expression object in the form
* "(field NOT IN (value1, value2) OR field IS NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notInOrNull(
ExpressionInterface|string $field,
ExpressionInterface|array|string $values,
?string $type = null,
) {
$or = new static([], [], 'OR');
$or
->notIn($field, $values, $type)
->isNull($field);
return $this->add($or);
}
/**
* Adds a new condition to the expression object in the form "EXISTS (...)".
*
* @param \Cake\Database\ExpressionInterface $expression the inner query
* @return $this
*/
public function exists(ExpressionInterface $expression)
{
return $this->add(new UnaryExpression('EXISTS', $expression, UnaryExpression::PREFIX));
}
/**
* Adds a new condition to the expression object in the form "NOT EXISTS (...)".
*
* @param \Cake\Database\ExpressionInterface $expression the inner query
* @return $this
*/
public function notExists(ExpressionInterface $expression)
{
return $this->add(new UnaryExpression('NOT EXISTS', $expression, UnaryExpression::PREFIX));
}
/**
* Adds a new condition to the expression object in the form
* "field BETWEEN from AND to".
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values in between the range.
* @param mixed $from The initial value of the range.
* @param mixed $to The ending value in the comparison range.
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function between(ExpressionInterface|string $field, mixed $from, mixed $to, ?string $type = null)
{
$type ??= $this->_calculateType($field);
return $this->add(new BetweenExpression($field, $from, $to, $type));
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "AND"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return static
*/
public function and(ExpressionInterface|Closure|array|string $conditions, array $types = []): static
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types)));
}
return new static($conditions, $this->getTypeMap()->setTypes($types));
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "OR"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return static
*/
public function or(ExpressionInterface|Closure|array|string $conditions, array $types = []): static
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
}
return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
}
/**
* Adds a new set of conditions to this level of the tree and negates
* the final result by prepending a NOT, it will look like
* "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one
* currently configured for this object.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be added and negated
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return $this
*/
public function not(ExpressionInterface|Closure|array|string $conditions, array $types = [])
{
return $this->add(['NOT' => $conditions], $types);
}
/**
* Returns the number of internal conditions that are stored in this expression.
* Useful to determine if this expression object is void or it will generate
* a non-empty string when compiled
*
* @return int
*/
public function count(): int
{
return count($this->_conditions);
}
/**
* Builds equal condition or assignment with identifier wrapping.
*
* @param string $leftField Left join condition field name.
* @param string $rightField Right join condition field name.
* @return $this
*/
public function equalFields(string $leftField, string $rightField)
{
$wrapIdentifier = function ($field): ExpressionInterface {
if ($field instanceof ExpressionInterface) {
return $field;
}
return new IdentifierExpression($field);
};
return $this->eq($wrapIdentifier($leftField), $wrapIdentifier($rightField));
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$len = $this->count();
if ($len === 0) {
return '';
}
$conjunction = $this->_conjunction;
$template = $len === 1 ? '%s' : '(%s)';
$parts = [];
foreach ($this->_conditions as $part) {
if ($part instanceof Query) {
$part = '(' . $part->sql($binder) . ')';
} elseif ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
}
if ($part !== '') {
$parts[] = $part;
}
}
return sprintf($template, implode(" {$conjunction} ", $parts));
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach ($this->_conditions as $c) {
if ($c instanceof ExpressionInterface) {
$callback($c);
$c->traverse($callback);
}
}
return $this;
}
/**
* Executes a callback for each of the parts that form this expression.
*
* The callback is required to return a value with which the currently
* visited part will be replaced. If the callback returns null then
* the part will be discarded completely from this expression.
*
* The callback function will receive each of the conditions as first param and
* the key as second param. It is possible to declare the second parameter as
* passed by reference, this will enable you to change the key under which the
* modified part is stored.
*
* @param \Closure $callback The callback to run for each part
* @return $this
*/
public function iterateParts(Closure $callback)
{
$parts = [];
foreach ($this->_conditions as $k => $c) {
$key = &$k;
$part = $callback($c, $key);
if ($part !== null) {
$parts[$key] = $part;
}
}
$this->_conditions = $parts;
return $this;
}
/**
* Returns true if this expression contains any other nested
* ExpressionInterface objects
*
* @return bool
*/
public function hasNestedExpression(): bool
{
foreach ($this->_conditions as $c) {
if ($c instanceof ExpressionInterface) {
return true;
}
}
return false;
}
/**
* Auxiliary function used for decomposing a nested array of conditions and build
* a tree structure inside this object to represent the full SQL expression.
* String conditions are stored directly in the conditions, while any other
* representation is wrapped around an adequate instance or of this class.
*
* @param array $conditions list of conditions to be stored in this object
* @param array<int|string, string> $types list of types associated on fields referenced in $conditions
* @return void
*/
protected function _addConditions(array $conditions, array $types): void
{
$operators = ['and', 'or', 'xor'];
$typeMap = $this->getTypeMap()->setTypes($types);
foreach ($conditions as $k => $c) {
$numericKey = is_numeric($k);
if ($c instanceof Closure) {
$expr = new static([], $typeMap);
$c = $c($expr, $this);
}
if ($numericKey && empty($c)) {
continue;
}
$isArray = is_array($c);
$isOperator = false;
$isNot = false;
if (!$numericKey) {
$normalizedKey = strtolower($k);
$isOperator = in_array($normalizedKey, $operators);
$isNot = $normalizedKey === 'not';
}
if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) {
continue;
}
if ($numericKey && $c instanceof ExpressionInterface) {
$this->_conditions[] = $c;
continue;
}
if ($numericKey && is_string($c)) {
$this->_conditions[] = $c;
continue;
}
if ($numericKey && $isArray || $isOperator) {
$this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k);
continue;
}
if ($isNot) {
$this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap));
continue;
}
if (!$numericKey) {
$this->_conditions[] = $this->_parseCondition($k, $c);
}
}
}
/**
* Parses a string conditions by trying to extract the operator inside it if any
* and finally returning either an adequate QueryExpression object or a plain
* string representation of the condition. This function is responsible for
* generating the placeholders and replacing the values by them, while storing
* the value elsewhere for future binding.
*
* @param string $condition The value from which the actual field and operator will
* be extracted.
* @param mixed $value The value to be bound to a placeholder for the field
* @return \Cake\Database\ExpressionInterface|string
* @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
*/
protected function _parseCondition(string $condition, mixed $value): ExpressionInterface|string
{
$expression = trim($condition);
$operator = '=';
$spaces = substr_count($expression, ' ');
// Handle expression values that contain multiple spaces, such as
// operators with a space in them like `field IS NOT` and
// `field NOT LIKE`, or combinations with function expressions
// like `CONCAT(first_name, ' ', last_name) IN`.
if ($spaces > 1) {
$parts = explode(' ', $expression);
if (preg_match('/(is not|not \w+)$/i', $expression)) {
$last = array_pop($parts);
$second = array_pop($parts);
$parts[] = "{$second} {$last}";
}
$operator = array_pop($parts);
$expression = implode(' ', $parts);
} elseif ($spaces == 1) {
$parts = explode(' ', $expression, 2);
[$expression, $operator] = $parts;
}
$operator = strtoupper(trim($operator));
$type = $this->getTypeMap()->type($expression);
$typeMultiple = (is_string($type) && str_contains($type, '[]'));
if (in_array($operator, ['IN', 'NOT IN']) || $typeMultiple) {
$type = $type ?: 'string';
if (!$typeMultiple) {
$type .= '[]';
}
$operator = $operator === '=' ? 'IN' : $operator;
$operator = $operator === '!=' ? 'NOT IN' : $operator;
$typeMultiple = true;
}
if ($typeMultiple) {
$value = $value instanceof ExpressionInterface ? $value : (array)$value;
}
if ($operator === 'IS' && $value === null) {
return new UnaryExpression(
'IS NULL',
new IdentifierExpression($expression),
UnaryExpression::POSTFIX,
);
}
if ($operator === 'IS NOT' && $value === null) {
return new UnaryExpression(
'IS NOT NULL',
new IdentifierExpression($expression),
UnaryExpression::POSTFIX,
);
}
if ($operator === 'IS' && $value !== null) {
$operator = '=';
}
if ($operator === 'IS NOT' && $value !== null) {
$operator = '!=';
}
if ($value === null && $this->_conjunction !== ',') {
throw new InvalidArgumentException(
sprintf(
'Expression `%s` has invalid `null` value.'
. ' If `null` is a valid value, operator (IS, IS NOT) is missing.',
$expression,
),
);
}
return new ComparisonExpression($expression, $value, $type, $operator);
}
/**
* Returns the type name for the passed field if it was stored in the typeMap
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to get a type for.
* @return string|null The computed type or null, if the type is unknown.
*/
protected function _calculateType(ExpressionInterface|string $field): ?string
{
$field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
if (!is_string($field)) {
return null;
}
return $this->getTypeMap()->type($field);
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
foreach ($this->_conditions as $i => $condition) {
if ($condition instanceof ExpressionInterface) {
$this->_conditions[$i] = clone $condition;
}
}
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* String expression with collation.
*/
class StringExpression implements ExpressionInterface
{
/**
* @var string
*/
protected string $string;
/**
* @var string
*/
protected string $collation;
/**
* @param string $string String value
* @param string $collation String collation
*/
public function __construct(string $string, string $collation)
{
$this->string = $string;
$this->collation = $collation;
}
/**
* Sets the string collation.
*
* @param string $collation String collation
* @return void
*/
public function setCollation(string $collation): void
{
$this->collation = $collation;
}
/**
* Returns the string collation.
*
* @return string
*/
public function getCollation(): string
{
return $this->collation;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $this->string, 'string');
return $placeholder . ' COLLATE ' . $this->collation;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
return $this;
}
}
@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
/**
* This expression represents SQL fragments that are used for comparing one tuple
* to another, one tuple to a set of other tuples or one tuple to an expression
*/
class TupleComparison extends ComparisonExpression
{
/**
* The type to be used for casting the value to a database representation
*
* @var array<string|null>
*/
protected array $types;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|array|string $fields the fields to use to form a tuple
* @param \Cake\Database\ExpressionInterface|array $values the values to use to form a tuple
* @param array<string|null> $types the types names to use for casting each of the values, only
* one type per position in the value array in needed
* @param string $conjunction the operator used for comparing field and value
*/
public function __construct(
ExpressionInterface|array|string $fields,
ExpressionInterface|array $values,
array $types = [],
string $conjunction = '=',
) {
$this->types = $types;
$this->setField($fields);
$this->_operator = $conjunction;
$this->setValue($values);
}
/**
* Returns the type to be used for casting the value to a database representation
*
* @return array<string|null>
*/
public function getType(): array
{
return $this->types;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue(mixed $value): void
{
if ($this->isMulti()) {
if (is_array($value) && !is_array(current($value))) {
throw new InvalidArgumentException(
'Multi-tuple comparisons require a multi-tuple value, single-tuple given.',
);
}
} elseif (is_array($value) && is_array(current($value))) {
throw new InvalidArgumentException(
'Single-tuple comparisons require a single-tuple value, multi-tuple given.',
);
}
$this->_value = $value;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$template = '(%s) %s (%s)';
$fields = [];
$originalFields = $this->getField();
if (!is_array($originalFields)) {
$originalFields = [$originalFields];
}
foreach ($originalFields as $field) {
$fields[] = $field instanceof ExpressionInterface ? $field->sql($binder) : $field;
}
$values = $this->_stringifyValues($binder);
$field = implode(', ', $fields);
return sprintf($template, $field, $this->_operator, $values);
}
/**
* Returns a string with the values as placeholders in a string to be used
* for the SQL version of this expression
*
* @param \Cake\Database\ValueBinder $binder The value binder to convert expressions with.
* @return string
*/
protected function _stringifyValues(ValueBinder $binder): string
{
$values = [];
$parts = $this->getValue();
if ($parts instanceof ExpressionInterface) {
return $parts->sql($binder);
}
foreach ($parts as $i => $value) {
if ($value instanceof ExpressionInterface) {
$values[] = $value->sql($binder);
continue;
}
$type = $this->types;
$isMultiOperation = $this->isMulti();
if (!$type) {
$type = null;
}
if ($isMultiOperation) {
$bound = [];
foreach ($value as $k => $val) {
$valType = $type && isset($type[$k]) ? $type[$k] : $type;
assert($valType === null || is_scalar($valType));
$bound[] = $this->_bindValue($val, $binder, $valType);
}
$values[] = sprintf('(%s)', implode(',', $bound));
continue;
}
$valType = $type && isset($type[$i]) ? $type[$i] : $type;
assert($valType === null || is_scalar($valType));
$values[] = $this->_bindValue($value, $binder, $valType);
}
return implode(', ', $values);
}
/**
* @inheritDoc
*/
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('tuple');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
$fields = (array)$this->getField();
foreach ($fields as $field) {
$this->_traverseValue($field, $callback);
}
$value = $this->getValue();
if ($value instanceof ExpressionInterface) {
$callback($value);
$value->traverse($callback);
return $this;
}
foreach ($value as $val) {
if ($this->isMulti()) {
foreach ($val as $v) {
$this->_traverseValue($v, $callback);
}
} else {
$this->_traverseValue($val, $callback);
}
}
return $this;
}
/**
* Conditionally executes the callback for the passed value if
* it is an ExpressionInterface
*
* @param mixed $value The value to traverse
* @param \Closure $callback The callback to use when traversing
* @return void
*/
protected function _traverseValue(mixed $value, Closure $callback): void
{
if ($value instanceof ExpressionInterface) {
$callback($value);
$value->traverse($callback);
}
}
/**
* Determines if each of the values in this expressions is a tuple in
* itself
*
* @return bool
*/
public function isMulti(): bool
{
return in_array(strtolower($this->_operator), ['in', 'not in']);
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object that represents an expression with only a single operand.
*/
class UnaryExpression implements ExpressionInterface
{
/**
* Indicates that the operation is in pre-order
*
* @var int
*/
public const PREFIX = 0;
/**
* Indicates that the operation is in post-order
*
* @var int
*/
public const POSTFIX = 1;
/**
* The operator this unary expression represents
*
* @var string
*/
protected string $_operator;
/**
* Holds the value which the unary expression operates
*
* @var mixed
*/
protected mixed $_value;
/**
* Where to place the operator
*
* @var int
*/
protected int $position;
/**
* Constructor
*
* @param string $operator The operator to used for the expression
* @param mixed $value the value to use as the operand for the expression
* @param int $position either UnaryExpression::PREFIX or UnaryExpression::POSTFIX
*/
public function __construct(string $operator, mixed $value, int $position = self::PREFIX)
{
$this->_operator = $operator;
$this->_value = $value;
$this->position = $position;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$operand = $this->_value;
if ($operand instanceof ExpressionInterface) {
$operand = $operand->sql($binder);
}
if ($this->position === self::POSTFIX) {
return '(' . $operand . ') ' . $this->_operator;
}
return $this->_operator . ' (' . $operand . ')';
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_value instanceof ExpressionInterface) {
$callback($this->_value);
$this->_value->traverse($callback);
}
return $this;
}
/**
* Perform a deep clone of the inner expression.
*
* @return void
*/
public function __clone()
{
if ($this->_value instanceof ExpressionInterface) {
$this->_value = clone $this->_value;
}
}
}
@@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypeMap;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object to contain values being inserted.
*
* Helps generate SQL with the correct number of placeholders and bind
* values correctly into the statement.
*/
class ValuesExpression implements ExpressionInterface
{
use ExpressionTypeCasterTrait;
use TypeMapTrait;
/**
* Array of values to insert.
*
* @var array
*/
protected array $_values = [];
/**
* List of columns to ensure are part of the insert.
*
* @var array
*/
protected array $_columns = [];
/**
* The Query object to use as a values expression
*
* @var \Cake\Database\Query|null
*/
protected ?Query $_query = null;
/**
* Whether values have been casted to expressions
* already.
*
* @var bool
*/
protected bool $_castedExpressions = false;
/**
* Constructor
*
* @param array $columns The list of columns that are going to be part of the values.
* @param \Cake\Database\TypeMap $typeMap A dictionary of column -> type names
*/
public function __construct(array $columns, TypeMap $typeMap)
{
$this->_columns = $columns;
$this->setTypeMap($typeMap);
}
/**
* Add a row of data to be inserted.
*
* @param \Cake\Database\Query|array $values Array of data to append into the insert, or
* a query for doing INSERT INTO .. SELECT style commands
* @return void
* @throws \Cake\Database\Exception\DatabaseException When mixing array and Query data types.
*/
public function add(Query|array $values): void
{
if (
(
count($this->_values) &&
$values instanceof Query
) ||
(
$this->_query &&
is_array($values)
)
) {
throw new DatabaseException(
'You cannot mix subqueries and array values in inserts.',
);
}
if ($values instanceof Query) {
$this->setQuery($values);
return;
}
$this->_values[] = $values;
$this->_castedExpressions = false;
}
/**
* Sets the columns to be inserted.
*
* @param array $columns Array with columns to be inserted.
* @return $this
*/
public function setColumns(array $columns)
{
$this->_columns = $columns;
$this->_castedExpressions = false;
return $this;
}
/**
* Gets the columns to be inserted.
*
* @return array
*/
public function getColumns(): array
{
return $this->_columns;
}
/**
* Get the bare column names.
*
* Because column names could be identifier quoted, we
* need to strip the identifiers off of the columns.
*
* @return array
*/
protected function _columnNames(): array
{
$columns = [];
foreach ($this->_columns as $col) {
if (is_string($col)) {
$col = trim($col, '`[]"');
}
$columns[] = $col;
}
return $columns;
}
/**
* Sets the values to be inserted.
*
* @param array $values Array with values to be inserted.
* @return $this
*/
public function setValues(array $values)
{
$this->_values = $values;
$this->_castedExpressions = false;
return $this;
}
/**
* Gets the values to be inserted.
*
* @return array
*/
public function getValues(): array
{
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
return $this->_values;
}
/**
* Sets the query object to be used as the values expression to be evaluated
* to insert records in the table.
*
* @param \Cake\Database\Query $query The query to set
* @return $this
*/
public function setQuery(Query $query)
{
$this->_query = $query;
return $this;
}
/**
* Gets the query object to be used as the values expression to be evaluated
* to insert records in the table.
*
* @return \Cake\Database\Query|null
*/
public function getQuery(): ?Query
{
return $this->_query;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
if (!$this->_values && $this->_query === null) {
return '';
}
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
$columns = $this->_columnNames();
$defaults = array_fill_keys($columns, null);
$placeholders = [];
$types = [];
$typeMap = $this->getTypeMap();
foreach ($defaults as $col => $v) {
$types[$col] = $typeMap->type($col);
}
foreach ($this->_values as $row) {
$row += $defaults;
$rowPlaceholders = [];
foreach ($columns as $column) {
$value = $row[$column];
if ($value instanceof ExpressionInterface) {
$rowPlaceholders[] = '(' . $value->sql($binder) . ')';
continue;
}
$placeholder = $binder->placeholder('c');
$rowPlaceholders[] = $placeholder;
$binder->bind($placeholder, $value, $types[$column]);
}
$placeholders[] = implode(', ', $rowPlaceholders);
}
$query = $this->getQuery();
if ($query) {
return ' ' . $query->sql($binder);
}
return sprintf(' VALUES (%s)', implode('), (', $placeholders));
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_query) {
return $this;
}
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
foreach ($this->_values as $v) {
if ($v instanceof ExpressionInterface) {
$v->traverse($callback);
}
if (!is_array($v)) {
continue;
}
foreach ($v as $field) {
if ($field instanceof ExpressionInterface) {
$callback($field);
$field->traverse($callback);
}
}
}
return $this;
}
/**
* Converts values that need to be casted to expressions
*
* @return void
*/
protected function _processExpressions(): void
{
$types = [];
$typeMap = $this->getTypeMap();
$columns = $this->_columnNames();
foreach ($columns as $c) {
if (!is_string($c) && !is_int($c)) {
continue;
}
$types[$c] = $typeMap->type($c);
}
$types = $this->_requiresToExpressionCasting($types);
if (!$types) {
return;
}
foreach ($this->_values as $row => $values) {
foreach ($types as $col => $type) {
/** @var \Cake\Database\Type\ExpressionTypeInterface $type */
$this->_values[$row][$col] = $type->toExpression($values[$col]);
}
}
$this->_castedExpressions = true;
}
}

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