Version v0.16.0-dev

This commit is contained in:
github-actions[bot] 2025-11-07 18:07:50 +00:00
parent e15e7dee90
commit ba8039b926
360 changed files with 12150 additions and 16362 deletions

View File

@ -1 +1 @@
v0.15.1 v0.16.0-dev

View File

@ -0,0 +1,26 @@
.action-link {
color: var(--control-color, @control-color);
}
.button-link {
.action-link();
.rounded-corners(3px);
background: var(--default-input-bg, @default-input-bg);
display: inline-block;
padding: 0.25em 0.5em;
&:hover {
background: var(--default-input-hover-bg, @default-input-hover-bg);
text-decoration: none;
}
&[aria-disabled="true"] {
background: none;
border: 1px solid var(--control-disabled-color, @control-disabled-color);
color: var(--control-disabled-color, @control-disabled-color);
cursor: not-allowed;
.user-select(none);
}
}

View File

@ -2,18 +2,27 @@
.ball { .ball {
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-flex;
text-align: center; align-items: center;
justify-content: center;
} }
.ball-size-xs { .ball-size-xs {
height: 1/3em; height: 1/3em;
width: 1/3em; width: 1/3em;
i.icon, span {
display: none;
}
} }
.ball-size-s { .ball-size-s {
height: 0.5em; height: 0.5em;
width: 0.5em; width: 0.5em;
i.icon, span {
display: none;
}
} }
.ball-size-m { .ball-size-m {
@ -21,10 +30,14 @@
width: 0.75em; width: 0.75em;
line-height: 0; line-height: 0;
i.icon:before { i.icon::before {
font-size: .75 - @ball-pad * 2; font-size: .75 - @ball-pad * 2;
line-height: 1em; line-height: 1em;
} }
span {
display: none;
}
} }
.ball-size-ml { .ball-size-ml {
@ -35,11 +48,15 @@
i.icon { i.icon {
line-height: 0.3; line-height: 0.3;
&:before { &::before {
font-size: 0.8 - @ball-pad * 2; font-size: 0.8 - @ball-pad * 2;
line-height: 1 - @ball-pad * 2; line-height: 1 - @ball-pad * 2;
} }
} }
span {
display: none;
}
} }
.ball-size-l { .ball-size-l {
@ -47,7 +64,7 @@
width: 1.5em; width: 1.5em;
line-height: 1em; line-height: 1em;
i.icon:before { i.icon::before, span {
font-size: 1 - @ball-pad * 2; font-size: 1 - @ball-pad * 2;
line-height: 1.5 - @ball-pad * 2; line-height: 1.5 - @ball-pad * 2;
} }
@ -57,7 +74,7 @@
width: 2em; width: 2em;
height: 2em; height: 2em;
i.icon:before { i.icon::before, span {
line-height: 2 - @ball-pad * 2; line-height: 2 - @ball-pad * 2;
} }
} }
@ -75,7 +92,7 @@
.state-ball { .state-ball {
.ball(); .ball();
&.state-pending:not(.ball-size-l):not(.ball-size-xl) { &.state-pending:not(.ball-size-l, .ball-size-xl) {
.ball-solid(var(--state-pending, @state-pending)); .ball-solid(var(--state-pending, @state-pending));
} }
@ -84,7 +101,7 @@
.ball-outline(var(--state-pending, @state-pending)); .ball-outline(var(--state-pending, @state-pending));
} }
&.state-up:not(.ball-size-l):not(.ball-size-xl) { &.state-up:not(.ball-size-l, .ball-size-xl) {
.ball-solid(var(--state-up, @state-up)); .ball-solid(var(--state-up, @state-up));
} }
@ -97,7 +114,7 @@
.ball-solid(var(--state-down, @state-down)); .ball-solid(var(--state-down, @state-down));
} }
&.state-ok:not(.ball-size-l):not(.ball-size-xl) { &.state-ok:not(.ball-size-l, .ball-size-xl) {
.ball-solid(var(--state-ok, @state-ok)); .ball-solid(var(--state-ok, @state-ok));
} }
@ -124,7 +141,6 @@
i.icon { i.icon {
text-align: center; text-align: center;
display: block;
&::before { &::before {
margin-right: 0; margin-right: 0;
@ -133,13 +149,13 @@
// Specific icon styles // Specific icon styles
&.ball-size-l i { &.ball-size-l i {
&.fa-sitemap:before { &.fa-sitemap::before {
font-size: 8px; // px to ignore browser min font-size font-size: 8px; // px to ignore browser min font-size
} }
} }
&.ball-size-xl i { &.ball-size-xl i {
&.fa-sitemap:before { &.fa-sitemap::before {
font-size: .857em; font-size: .857em;
line-height: (2 - @ball-pad * 2) / .857; line-height: (2 - @ball-pad * 2) / .857;
} }

View File

@ -2,10 +2,15 @@
.icinga-controls { .icinga-controls {
.uploaded-files { .uploaded-files {
background-color: @default-input-bg; background-color: var(--default-input-bg, @default-input-bg);
} }
} }
// Icinga Web < 2.13
.button-link[aria-disabled="true"]:hover {
background: none;
}
form.icinga-form { form.icinga-form {
.uploaded-files { .uploaded-files {
flex: 1 1 auto; flex: 1 1 auto;
@ -20,6 +25,14 @@ form.icinga-form {
} }
} }
.icinga-controls {
.required-hint {
font-weight: bold;
color: var(--default-text-color-light, @default-text-color-light);
}
}
// Button styles // Button styles
// The `form` selector is only required to overrule the hover effect applied by Icinga Web. // The `form` selector is only required to overrule the hover effect applied by Icinga Web.
@ -86,3 +99,51 @@ form.icinga-form .control-group {
} }
} }
} }
// suggestion-element style
form.icinga-form .suggestion-element-group {
flex: 1 1 auto;
.suggestion-element {
border-radius: 0 0.25em 0.25em 0;
}
}
.module-icingadb {
// Icinga DB Web (legacy) table header layout (e.g. in group details)
> .controls {
> .table-row {
display: flex;
gap: .5em;
> .col.title {
margin-right: auto;
}
}
}
// Icinga DB Web (legacy) object grid layout
> .content > .item-table.group-grid:has(.col.title) {
grid-template-columns: repeat(auto-fit, 15em) !important;
> .group-grid-cell {
display: revert;
&::before, &::after {
display: none !important;
}
> .col.title {
border: none;
> .column-content {
overflow: hidden;
> * {
.text-ellipsis();
}
}
}
}
}
}

View File

@ -16,7 +16,7 @@
display: block; display: block;
} }
i:before { i::before {
margin: 0; margin: 0;
} }
} }
@ -52,7 +52,7 @@
} }
} }
i.icon:before { i.icon::before {
color: inherit; color: inherit;
} }
} }
@ -69,7 +69,7 @@
height: 100%; height: 100%;
} }
i.icon:before { i.icon::before {
margin-right: 0; margin-right: 0;
} }
} }
@ -154,11 +154,6 @@
} }
> .search-controls > .search-bar .filter-input-area { > .search-controls > .search-bar .filter-input-area {
label { --term-padding-v: 0px;
&::after,
input {
padding: 0 .5em;
}
}
} }
} }

View File

@ -1,5 +1,5 @@
.empty-state { .empty-state {
color: @empty-state-color; color: var(--empty-state-color, @empty-state-color);
} }
.empty-state-bar { .empty-state-bar {
@ -7,5 +7,6 @@
text-align: center; text-align: center;
.rounded-corners(); .rounded-corners();
background-color: @empty-state-bar-bg; background-color: var(--empty-state-bar-bg, @empty-state-bar-bg);
color: var(--default-text-color, @default-text-color);
} }

View File

@ -1,5 +1,5 @@
@font-face { @font-face {
font-family: 'Icinga-Icons'; font-family: Icinga-Icons;
src: url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.ttf') format('truetype'), src: url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.ttf') format('truetype'),
url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.woff') format('woff'), url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.woff') format('woff'),
url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.svg') format('svg'); url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.svg') format('svg');
@ -8,9 +8,9 @@
font-display: block; font-display: block;
} }
[class^="iicon-"]:before, [class*=" iicon-"]:before { [class^="iicon-"]::before, [class*=" iicon-"]::before {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
font-family: 'Icinga-Icons'; font-family: Icinga-Icons;
speak: none; speak: none;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
@ -23,42 +23,51 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.iicon-certificate:before { .iicon-add-inside::before {
content: "\e90c";
}
.iicon-insert-group::before {
content: "\e90d";
}
.iicon-wrap::before {
content: "\e90e";
}
.iicon-certificate::before {
content: "\e906"; content: "\e906";
} }
.iicon-filter-check-circle:before { .iicon-filter-check-circle::before {
content: "\e90b"; content: "\e90b";
} }
.iicon-ca-check-circle:before { .iicon-ca-check-circle::before {
content: "\e908"; content: "\e908";
} }
.iicon-refresh-cert:before { .iicon-refresh-cert::before {
content: "\e909"; content: "\e909";
} }
.iicon-th-list:before { .iicon-th-list::before {
content: "\e90a"; content: "\e90a";
} }
.iicon-icinga:before { .iicon-icinga::before {
content: "\e907"; content: "\e907";
} }
.iicon-minimal:before, .iicon-minimal::before,
.iicon-list-view-minimal:before { .iicon-list-view-minimal::before {
content: "\e900"; content: "\e900";
} }
.iicon-detailed:before, .iicon-detailed::before,
.iicon-list-view-detailed:before { .iicon-list-view-detailed::before {
content: "\e901"; content: "\e901";
} }
.iicon-default:before, .iicon-default::before,
.iicon-list-view-default:before { .iicon-list-view-default::before {
content: "\e902"; content: "\e902";
} }
.iicon-grid:before { .iicon-grid::before {
content: "\e903"; content: "\e903";
} }
.iicon-bracket-open:before { .iicon-bracket-open::before {
content: "\e904"; content: "\e904";
} }
.iicon-bracket-close:before { .iicon-bracket-close::before {
content: "\e905"; content: "\e905";
} }

177
asset/css/item-layout.less Normal file
View File

@ -0,0 +1,177 @@
// Layout
.item-layout {
// Note that mode specific rules are as strict as possible to avoid conflicts with nested layouts.
// Consider an item which contains another item in a different layout mode. Coincidentally, this is
// already the case in Icinga DB Web with the last comment flyout.
.flowing-content(@layout) when (@layout = "default") {
display: inline-flex;
align-items: baseline;
white-space: nowrap;
min-width: 0;
column-gap: .28125em; // calculated &nbsp; width
> .ellipsize, > .subject { // .subject is compat only, Icinga DB Web used it thoroughly
.text-ellipsis();
}
}
.flowing-content(@layout) when (@layout = "detailed") {
display: inline-flex;
align-items: baseline;
flex-wrap: wrap;
column-gap: .28125em; // calculated &nbsp; width
}
display: flex;
.visual {
display: flex;
flex-direction: column;
width: auto;
padding: .25em 0;
margin-right: 1em;
> i.icon {
font-size: 1.5em;
&::before {
margin-right: 0;
}
}
}
.main {
flex: 1 1 auto;
padding: .25em 0;
width: 0;
}
header {
display: flex;
align-items: baseline;
justify-content: space-between;
> .extended-info {
flex-shrink: 0;
}
}
.caption {
p {
display: inline-block;
}
img {
max-height: 1em;
}
}
footer {
display: flex;
justify-content: space-between;
padding-top: .5em;
}
.title {
margin-right: 1em;
}
&.minimal-item-layout > .main > header {
max-width: 100%;
height: 1.5em;
> .caption {
flex: 1 1 auto;
height: 1.5em;
width: 0;
&:not(:empty) {
margin-right: 1em;
}
.text-ellipsis();
}
}
&.default-item-layout > .main {
> header {
height: 1.5em;
> .title {
.flowing-content("default");
}
> .extended-info {
.flowing-content("default");
}
}
> .caption {
height: 1.5em;
.text-ellipsis();
}
}
&.detailed-item-layout > .main {
> header {
> .title {
.flowing-content("detailed");
word-break: break-word;
hyphens: auto;
}
> .extended-info {
.flowing-content("detailed");
}
}
> .caption {
display: block;
overflow: hidden;
position: relative;
.line-clamp(5);
}
}
}
// Style
.item-layout {
color: var(--default-text-color-light, @default-text-color-light);
.caption {
i {
opacity: 0.8;
}
a {
color: var(--default-text-color, @default-text-color);
}
}
.title {
.subject {
color: var(--default-text-color, @default-text-color);
}
a {
color: var(--default-text-color, @default-text-color);
font-weight: bold;
&:hover {
color: var(--link-hover-color, @link-hover-color);
text-decoration: none;
.subject {
color: var(--link-hover-color, @link-hover-color);
}
}
}
}
}

View File

@ -4,82 +4,35 @@
list-style-type: none; list-style-type: none;
} }
.content:has(> .item-list) > .item-list > .empty-state {
.empty-state-bar();
}
// Layout // Layout
.item-list { .item-list {
margin: 0; margin: 0;
padding: 0; padding: 0;
.list-item {
display: flex;
.main {
flex: 1 1 auto;
padding: .5em 0;
width: 0;
margin-left: .5em;
}
.visual {
display: flex;
align-items: center;
flex-direction: column;
}
.caption {
height: 3em;
text-overflow: ellipsis;
overflow: hidden;
.line-clamp();
img {
max-height: 1em;
}
}
header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
footer {
display: flex;
justify-content: space-between;
}
}
> .empty-state-bar { > .empty-state-bar {
margin: 0 1em; margin: 0 1em;
} }
} }
.item-list.default-layout .list-item {
.title {
display: inline-flex;
align-items: baseline;
white-space: nowrap;
min-width: 0;
> * {
margin: 0 .28125em; // 0 calculated &nbsp; width
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.subject {
.text-ellipsis();
}
}
}
.controls .list-item:not(:last-child) { .controls .list-item:not(:last-child) {
margin-bottom: .5em; margin-bottom: .5em;
} }
:not(.dashboard) > .container > .content:has(> .item-list), // compat only, for Icinga Web (See #286)
.content:has(> .item-list) {
padding-left: 0;
padding-right: 0;
> .item-list > .list-item {
padding-left: 1em;
padding-right: 1em;
}
> .item-list > .empty-state {
margin: 0 1em;
}
}

View File

@ -4,79 +4,116 @@ ul.item-table {
list-style-type: none; list-style-type: none;
} }
.table-row { .content:has(> .item-table) > .item-table > .empty-state {
color: @default-text-color-light; .empty-state-bar();
.title {
.subject {
color: @default-text-color;
}
a {
font-weight: bold;
&:hover {
color: @list-item-title-hover-color;
text-decoration: none;
}
}
}
}
@media print {
.item-table li.page-break-follows:not(:last-of-type) {
.col {
border-bottom: none;
}
.visual {
margin-bottom: 0;
}
}
} }
// Layout // Layout
.table-row { ul.item-table {
.title { // Grid specific rules
display: flex; display: grid;
grid-template-columns: minmax(0, 1fr) repeat(var(--columns), auto);
&:has(> li > .visual) {
grid-template-columns: auto minmax(0, 1fr) repeat(var(--columns), auto);
}
.visual { > li {
width: 2.5em; display: contents;
padding: .5em 0;
margin-top: -.5em; &.item-layout .main {
margin-bottom: -.5em; // Usually, the parent is flex, but here it's contents. .main is still stretched though,
// because it's a grid item with a width of 1fr. But the default-item-layout sets a width
// which needs to be overridden.
width: auto;
} }
.column-content { .col, &::before, &::after {
flex: 1 1 auto; // The li might get a background on hover. Though, this won't be visible
width: 0; // as it has no box model since we apply display:contents to it.
background-color: inherit;
}
}
}
> * { :not(.dashboard) > .container > .content:has(> .item-table), // compat only, for Icinga Web (See #286)
.text-ellipsis(); .content:has(> .item-table) {
padding-left: 0;
padding-right: 0;
> .item-table > .empty-state {
margin: 0 1em;
}
> ul.item-table {
// Again, since the li has no box model, it cannot have padding. So the first
// and last child need to get the left and right padding respectively.
// But we don't want to have a border that spans to the very right or left,
// so pseudo elements are required. We could add empty cells instead, but
// that would require hard coding the width here, which I'd like to avoid.
grid-template-columns: ~"auto minmax(0, 1fr) repeat(var(--columns), auto) auto";
&:has(> li > .visual) {
grid-template-columns: ~"auto auto minmax(0, 1fr) repeat(var(--columns), auto) auto";
}
> li.table-row {
&::before, &::after {
display: inline-block;
content: '\00a0';
width: 0;
margin-bottom: 1px;
}
&::before {
padding-left: 1em;
}
&::after {
padding-right: 1em;
} }
} }
} }
.col {
white-space: nowrap;
}
}
ul.item-table {
display: grid;
> .table-row {
.col:not(.title) {
display: grid;
align-items: center;
}
}
} }
ul.item-table { ul.item-table {
// General rules
padding: 0; padding: 0;
margin: 0; margin: 0;
.table-row {
.col {
margin-right: 0; // Otherwise background has gaps
padding: .5em 1em .5em 0;
&:last-child {
padding-right: 0;
}
.title {
margin-right: 0;
}
}
// This is for the legacy layout only
// TODO: Drop this together with BaseTableRowItem
.col.title:has(> .visual) {
display: flex;
> .visual {
padding-right: .5em;
}
}
&:not(:last-of-type) {
.col {
border-bottom: 1px solid @list-item-separation-bg;
&.visual {
border-color: @default-bg;
}
}
}
}
} }
div.item-table { div.item-table {
@ -85,11 +122,10 @@ div.item-table {
} }
} }
div.table-row { @media print {
display: flex; .item-table li.page-break-follows:not(:last-of-type) {
column-gap: 1em; .col {
border-bottom: none;
.title { }
flex: 1 1 auto;
} }
} }

View File

@ -1,8 +1,6 @@
// Style // Style
.list-item { .list-item {
color: @default-text-color-light;
&:not(:first-child) > .main { &:not(:first-child) > .main {
border-top: 1px solid @list-item-separation-bg; border-top: 1px solid @list-item-separation-bg;
} }
@ -10,40 +8,6 @@
&:not(:first-child) .visual { &:not(:first-child) .visual {
margin-top: 1px; margin-top: 1px;
} }
.caption {
i {
opacity: 0.8;
}
a {
color: @default-text-color;
}
}
.title {
.subject {
color: @default-text-color;
}
a {
color: @default-text-color;
font-weight: bold;
&:hover {
color: @list-item-title-hover-color;
text-decoration: none;
.subject {
color: @list-item-title-hover-color;
}
}
}
}
footer {
padding-top: .5em;
}
} }
@media print { @media print {
@ -56,34 +20,10 @@
// Layout // Layout
.list-item { .list-item.item-layout {
.main,
.visual { .visual {
padding: .5em 0; padding-top: .5em;
width: 2.5em; padding-bottom: .5em;
}
.caption {
p {
display: inline-block;
}
}
.title {
margin-right: 1em;
p {
margin: 0;
}
}
time {
white-space: nowrap;
}
footer {
> * {
font-size: .857em;
line-height: 1.5*.857em;
}
} }
} }

View File

@ -39,3 +39,8 @@
.monospace-font() { .monospace-font() {
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
} }
.user-select(@user-select) {
-webkit-user-select: @user-select;
user-select: @user-select;
}

View File

@ -19,7 +19,7 @@
} }
&:disabled { &:disabled {
color: @schedule-element-fields-disabled-color; color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color);
} }
} }
} }
@ -39,7 +39,7 @@
.monthly, .ordinal:not(.annually) { .monthly, .ordinal:not(.annually) {
padding: .5em; padding: .5em;
margin-left: -.5em; margin-left: -.5em;
border: 1px solid @schedule-element-fields-border-color; border: 1px solid var(--schedule-element-fields-border-color, @schedule-element-fields-border-color);
.rounded-corners(.75em); .rounded-corners(.75em);
} }
@ -56,13 +56,13 @@
pointer-events: none; pointer-events: none;
label { label {
color: @schedule-element-fields-disabled-color; color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color);
background-color: @schedule-element-fields-disabled-bg; background-color: var(--schedule-element-fields-disabled-bg, @schedule-element-fields-disabled-bg);
} }
input:checked + label { input:checked + label {
background: @schedule-element-fields-disabled-selected-bg; background: var(--schedule-element-fields-disabled-selected-bg, @schedule-element-fields-disabled-selected-bg);
color: @schedule-element-fields-disabled-color; color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color);
} }
} }
@ -75,11 +75,11 @@
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
padding: .75em 0; padding: .75em 0;
background: @schedule-element-fields-bg; background: var(--schedule-element-fields-bg, @schedule-element-fields-bg);
color: @schedule-element-fields-color; color: var(--schedule-element-fields-color, @schedule-element-fields-color);
&:hover { &:hover {
background-color: @schedule-element-fields-hover-bg; background-color: var(--schedule-element-fields-hover-bg, @schedule-element-fields-hover-bg);
} }
&:focus { &:focus {
@ -88,27 +88,27 @@
} }
input:checked + label { input:checked + label {
background-color: @schedule-element-fields-selected-bg; background-color: var(--schedule-element-fields-selected-bg, @schedule-element-fields-selected-bg);
color: @schedule-element-fields-selected-color; color: var(--schedule-element-fields-selected-color, @schedule-element-fields-selected-color);
} }
input:checked + label:hover { input:checked + label:hover {
background-color: @schedule-element-fields-selected-hover-bg; background-color: var(--schedule-element-fields-selected-hover-bg, @schedule-element-fields-selected-hover-bg);
border-color: @schedule-element-fields-selected-hover-bg; border-color: var(--schedule-element-fields-selected-hover-bg, @schedule-element-fields-selected-hover-bg);
} }
} }
&.multiple-fields { &.multiple-fields {
li:not(:last-child) label { li:not(:last-child) label {
border-right: 1px solid @schedule-element-fields-border-color; border-right: 1px solid var(--schedule-element-fields-border-color, @schedule-element-fields-border-color);
} }
input:focus + label { input:focus + label {
box-shadow: inset 0 0 0 3px @schedule-element-fields-outline-color; box-shadow: inset 0 0 0 3px var(--schedule-element-fields-outline-color, @schedule-element-fields-outline-color);
} }
input:checked:focus + label { input:checked:focus + label {
box-shadow: inset 0 0 0 3px @schedule-element-fields-selected-outline-color; box-shadow: inset 0 0 0 3px var(--schedule-element-fields-selected-outline-color, @schedule-element-fields-selected-outline-color);
} }
} }
@ -128,7 +128,7 @@
} }
&:focus-within { &:focus-within {
outline: 3px solid @schedule-element-fields-outline-color; outline: 3px solid var(--schedule-element-fields-outline-color, @schedule-element-fields-outline-color);
outline-offset: 2px; outline-offset: 2px;
} }
@ -137,7 +137,7 @@
} }
input:checked + label:hover { input:checked + label:hover {
background-color: @schedule-element-fields-selected-bg; background-color: var(--schedule-element-fields-selected-bg, @schedule-element-fields-selected-bg);
} }
} }
} }
@ -145,20 +145,17 @@
.note { .note {
display: none; display: none;
padding: .5em; padding: .5em;
background: @schedule-element-keyboard-note-bg; background: var(--schedule-element-keyboard-note-bg, @schedule-element-keyboard-note-bg);
.rounded-corners(.25em); .rounded-corners(.25em);
text-align: center; text-align: center;
margin-top: 1em; margin-top: 1em;
line-height: 1.25; line-height: 1.25;
} }
/* .weekly */
.weekly { }
/* .monthly styles */ /* .monthly styles */
.monthly { .monthly {
li label { li label {
border-top: 1px solid @schedule-element-fields-border-color; border-top: 1px solid var(--schedule-element-fields-border-color, @schedule-element-fields-border-color);
} }
li:first-child, li:first-child,
@ -189,14 +186,13 @@
li:nth-child(4n) label { li:nth-child(4n) label {
margin-right: 0; margin-right: 0;
} }
}
.toggle-slider-controls { .toggle-slider-controls {
display: flex; display: flex;
column-gap: 1em; column-gap: 1em;
align-items: center; align-items: center;
margin-top: 1em; margin-bottom: -.6em;
margin-bottom: -.6em;
}
} }
} }
@ -205,6 +201,6 @@
padding-top: 0.5625em; padding-top: 0.5625em;
p { p {
color: @schedule-element-fields-disabled-color; color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color);
} }
} }

View File

@ -11,8 +11,8 @@
} }
// Submit button styles // Submit button styles
input[type=submit], input[type="submit"],
button[type=submit], button[type="submit"],
button:not([type]) { button:not([type]) {
background: var(--primary-button-bg, @primary-button-bg); background: var(--primary-button-bg, @primary-button-bg);
color: var(--primary-button-color, @primary-button-color); color: var(--primary-button-color, @primary-button-color);
@ -26,20 +26,20 @@
} }
// Hide the submit button, it must exist, but shouldn't be shown to the user // Hide the submit button, it must exist, but shouldn't be shown to the user
input[type=submit][value="hidden"] { input[type="submit"][value="hidden"] {
display: none; display: none;
} }
// Left-most search dropdown style // Left-most search dropdown style
button.search-options { button.search-options {
i.icon:before { i.icon::before {
font-size: 1.2em; font-size: 1.2em;
margin-right: 0; margin-right: 0;
color: var(--control-color, @control-color); color: var(--control-color, @control-color);
} }
&:disabled { &:disabled {
i.icon:before { i.icon::before {
color: var(--control-disabled-color, @control-disabled-color); color: var(--control-disabled-color, @control-disabled-color);
} }
} }
@ -52,7 +52,7 @@
background-color: var(--search-condition-remove-bg, @search-condition-remove-bg); background-color: var(--search-condition-remove-bg, @search-condition-remove-bg);
color: var(--search-condition-remove-color, @search-condition-remove-color); color: var(--search-condition-remove-color, @search-condition-remove-color);
&:after { &::after {
content: ""; content: "";
position: absolute; position: absolute;
width: .4em; width: .4em;
@ -77,7 +77,7 @@
.terms > .filter-condition:first-child button { .terms > .filter-condition:first-child button {
border-radius: 0 .4em .4em 0; border-radius: 0 .4em .4em 0;
&:before { &::before {
content: ""; content: "";
position: absolute; position: absolute;
width: .4em; width: .4em;
@ -92,14 +92,14 @@
border-bottom-right-radius: .4em; border-bottom-right-radius: .4em;
} }
&:after { &::after {
content: none; content: none;
} }
} }
.logical_operator, .logical-operator,
.grouping_operator_open, .grouping-operator-open,
.grouping_operator_close { .grouping-operator-close {
input { input {
.rounded-corners(); .rounded-corners();
background-color: var(--search-logical-operator-bg, @search-logical-operator-bg); background-color: var(--search-logical-operator-bg, @search-logical-operator-bg);
@ -108,9 +108,9 @@
} }
.operator, .operator,
.logical_operator, .logical-operator,
.grouping_operator_open, .grouping-operator-open,
.grouping_operator_close { .grouping-operator-close {
input { input {
text-align: center; text-align: center;
} }
@ -153,7 +153,7 @@
li { li {
display: inline; display: inline;
&:not(:first-of-type):before { &:not(:first-of-type)::before {
display: inline; display: inline;
content: ', '; content: ', ';
} }
@ -193,12 +193,12 @@
left: ~"calc(-2em - 2px)"; // That's min-width + margin-right of an operator left: ~"calc(-2em - 2px)"; // That's min-width + margin-right of an operator
line-height: 16/12; // 16 (px) desired / default font size (px) line-height: 16/12; // 16 (px) desired / default font size (px)
i:before { i::before {
margin-right: 0; margin-right: 0;
} }
} }
&:not(._hover_delay):hover button { &:not([data-hover-delay]):hover button {
display: inline; display: inline;
} }
} }
@ -210,9 +210,9 @@
} }
label { label {
&.logical_operator, &.logical-operator,
&.grouping_operator_open, &.grouping-operator-open,
&.grouping_operator_close { &.grouping-operator-close {
margin-left: 1px; // adds up to 2px with the previous term margin-left: 1px; // adds up to 2px with the previous term
margin-right: 2px; margin-right: 2px;
} }

View File

@ -58,20 +58,29 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
cursor: not-allowed; cursor: not-allowed;
} }
.invalid-reason { .term-input-area {
padding: .25em; .invalid-reason {
.rounded-corners(.25em); padding: .25em;
border: 1px solid black; .rounded-corners(.25em);
font-weight: bold; border: 1px solid black;
background: var(--search-term-invalid-reason-bg, @search-term-invalid-reason-bg); font-weight: bold;
background: var(--search-term-invalid-reason-bg, @search-term-invalid-reason-bg);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 2s, visibility 2s; transition: opacity 2s, visibility 2s;
&.visible {
opacity: 1; &.visible {
visibility: visible; opacity: 1;
transition: none; visibility: visible;
transition: none;
}
}
.remove-action {
background: var(--search-term-remove-action-bg, @search-term-remove-action-bg);
color: var(--search-term-remove-action-color, @search-term-remove-action-color);
.rounded-corners(0.25em);
} }
} }
@ -153,12 +162,15 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
// Layout // Layout
.search-bar .filter-input-area, .search-bar .filter-input-area,
.term-input-area:not(.vertical) { .term-input-area:not(.vertical) {
--term-padding-v: .25em;
--term-padding-h: .5em;
overflow: auto hidden; overflow: auto hidden;
overflow-x: overlay; // Not invalid, but proprietary feature by chrome/webkit
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
width: 100%; width: 100%;
height: ~"calc(2em + 10px)"; // Search bar height + approximate scrollbar height // input line-height + (input vertical padding * 2) + approximate scrollbar height
height: ~"calc(20px + calc(var(--term-padding-v) * 2) + 10px)";
// Lets inputs grow based on their contents, Inspired by https://css-tricks.com/auto-growing-inputs-textareas/ // Lets inputs grow based on their contents, Inspired by https://css-tricks.com/auto-growing-inputs-textareas/
label { label {
@ -170,7 +182,7 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
&::after, &::after,
input { input {
width: auto; width: auto;
padding: .25em .5em; padding: var(--term-padding-v) var(--term-padding-h);
resize: none; resize: none;
} }
@ -208,6 +220,11 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
margin-right: 1px; margin-right: 1px;
} }
} }
&.read-only [data-index] .remove-action {
line-height: 20/12;
padding: var(--term-padding-v) var(--term-padding-h);
}
} }
.term-input-area.vertical { .term-input-area.vertical {
@ -278,7 +295,6 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
position: relative; position: relative;
input { input {
padding-left: 1.5em;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
@ -305,6 +321,36 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
top: 85%; top: 85%;
left: .5em; left: .5em;
} }
.remove-action {
display: flex;
align-items: center;
visibility: visible;
position: absolute;
width: 100%;
top: 0;
line-height: normal;
padding: 0.5em;
cursor: pointer;
i.icon {
margin-left: auto;
}
.remove-action-label {
margin-right: auto;
.text-ellipsis();
}
}
input:invalid ~ .remove-action,
input.invalid ~ .remove-action {
pointer-events: none;
}
&:not(:hover) .remove-action {
visibility: hidden;
}
} }
} }
} }
@ -324,7 +370,7 @@ fieldset:disabled .term-input-area [data-drag-initiator] {
padding: 0; padding: 0;
li.suggestion-title { li.suggestion-title {
padding: 1.25em .625em 0 .625em; padding: 1.25em .625em 0;
} }
li.failure-message { li.failure-message {

View File

@ -28,7 +28,7 @@
.rounded-corners(0); .rounded-corners(0);
} }
i.icon:before { i.drag-initiator.icon::before {
color: var(--search-editor-control-color, @search-editor-control-color); color: var(--search-editor-control-color, @search-editor-control-color);
} }
@ -36,6 +36,10 @@
cursor: grab; cursor: grab;
} }
.remove-button {
color: var(--search-editor-remove-control-color, @search-editor-remove-control-color);
}
input[type="text"], select { input[type="text"], select {
border: none; border: none;
background: var(--search-term-bg, @search-term-bg); background: var(--search-term-bg, @search-term-bg);
@ -81,50 +85,6 @@
} }
} }
.buttons {
ul {
.rounded-corners();
.box-shadow(0, 0, .5em);
border: 1px solid var(--search-editor-context-menu-border-color, @search-editor-context-menu-border-color);
background: var(--search-editor-context-menu-bg, @search-editor-context-menu-bg);
li:not(:first-child) {
border-top: 1px solid var(--search-editor-context-menu-border-color, @search-editor-context-menu-border-color);
}
button:hover {
background: var(--primary-button-bg, @primary-button-bg);
color: var(--primary-button-color, @primary-button-color);
}
// Add rounded corners to buttons as well, otherwise their
// background is not rounded and overlaps the list's corners
:first-child button {
.rounded-corners();
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
:last-child button {
.rounded-corners();
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:before {
// The left pointing arrow
border-bottom: 1px solid var(--search-editor-context-menu-border-color, @search-editor-context-menu-border-color);
border-left: 1px solid var(--search-editor-context-menu-border-color, @search-editor-context-menu-border-color);
background: var(--search-editor-context-menu-bg, @search-editor-context-menu-bg);
}
}
&:hover i.icon:before {
.rounded-corners();
background: var(--primary-button-bg, @primary-button-bg);
color: var(--primary-button-color, @primary-button-color);
}
}
input[type="submit"] { input[type="submit"] {
.rounded-corners(); .rounded-corners();
background: var(--primary-button-bg, @primary-button-bg); background: var(--primary-button-bg, @primary-button-bg);
@ -156,17 +116,18 @@
li { li {
display: flex; display: flex;
> :not(:first-child) { > .buttons {
margin-left: @item-spacing; margin-left: @item-spacing;
} }
> select,
> fieldset {
margin-left: ~"calc(@{item-spacing} + var(--depth))";
}
} }
ol { ol {
padding-left: 1em; > li {
padding-bottom: @item-spacing;
> li:first-child,
> :not(.filter-chain) + li {
margin-top: @item-spacing; margin-top: @item-spacing;
} }
} }
@ -175,16 +136,13 @@
padding: 0 .5em; padding: 0 .5em;
} }
li > select {
margin-right: auto;
}
fieldset { fieldset {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
margin: 0; margin: 0;
padding: 0; padding: 0;
input[data-type="column"],
input[data-type="value"] { input[data-type="value"] {
flex: 1 1 auto; flex: 1 1 auto;
} }
@ -202,55 +160,35 @@
margin-left: .5em; margin-left: .5em;
} }
i.icon:before { i.drag-initiator {
width: 1.5em; // Not a fan, but the only way to have a known width
}
li > ul:not(.buttons) > li:first-child:not(:has(.drag-initiator)),
ol > li:not(:has(.drag-initiator)) {
padding-left: 1.5em;
}
i.icon::before {
margin: 0; margin: 0;
font-size: 1.5em; font-size: 1.5em;
line-height: 1.5; line-height: 1.5;
} }
.buttons { select + .buttons {
position: relative; display: flex;
width: 100%;
ul { > :last-child {
position: absolute; flex: 1 1 auto;
right: 32/12em; // Target distance @ default font size / default font size display: flex;
z-index: 1; justify-content: flex-end;
width: auto;
padding: 0;
display: none;
button {
z-index: 1;
width: 100%;
text-align: left;
white-space: nowrap;
}
&:before {
// The left pointing arrow
content: "";
display: block;
height: 1em;
transform: rotate(-135deg);
width: 1em;
z-index: 1;
position: absolute;
top: ((28/12)/2)-.5em; // ((First row pixels @ default font size / default font size) / 2) - own half width
right: -.5em;
}
}
&:hover ul {
display: block;
}
i.icon:before {
padding: ((28/18)-1)/2em; // (Container pixels / default font size) - line height / (padding-top,padding-bottom)
line-height: 1;
} }
} }
fieldset + .buttons {
display: flex;
width: auto;
}
.cancel-button { .cancel-button {
margin-top: 2em - @item-spacing; margin-top: 2em - @item-spacing;

View File

@ -0,0 +1,29 @@
.suggestion-element-group {
display: inline-flex;
.suggestion-element,
.suggestion-element-icon {
line-height: normal;
height: 2.25em;
padding: 0.5em;
background-color: var(--default-input-bg, @default-input-bg);
color: var(--default-text-color, @default-text-color);
}
.suggestion-element {
border: none;
outline: none;
border-radius: 0 0.25em 0.25em 0;
}
.suggestion-element-icon {
padding-right: 0;
border-radius: 0.25em 0 0 0.25em;
}
&:focus-within {
border-radius: 0.25em;
outline: 3px solid var(--default-input-outline-color, @default-input-outline-color);
outline-offset: 1px;
}
}

View File

@ -41,6 +41,12 @@
@default-text-color-light: fade(@default-text-color, 75%); @default-text-color-light: fade(@default-text-color, 75%);
@default-text-color-inverted: @default-bg; @default-text-color-inverted: @default-bg;
@default-input-bg: #404d72; @default-input-bg: #404d72;
@default-input-hover-bg: #434374;
@default-input-outline-color: @base-primary-light;
@default-remove-bg: @state-critical;
@default-remove-color: @default-text-color-inverted;
@default-delete-bg: @state-critical;
@default-delete-color: @default-text-color-inverted;
@state-ok: #44bb77; @state-ok: #44bb77;
@state-up: @state-ok; @state-up: @state-ok;
@ -54,6 +60,7 @@
@primary-button-color: @default-text-color-inverted; @primary-button-color: @default-text-color-inverted;
@primary-button-bg: @base-primary-bg; @primary-button-bg: @base-primary-bg;
@primary-button-hover-bg: @base-primary-dark; @primary-button-hover-bg: @base-primary-dark;
@link-hover-color: @base-primary-color;
@search-term-bg: @base-gray; @search-term-bg: @base-gray;
@search-term-color: @default-text-color-inverted; @search-term-color: @default-text-color-inverted;
@ -66,6 +73,8 @@
@search-term-highlighted-bg: @base-primary-bg; @search-term-highlighted-bg: @base-primary-bg;
@search-term-highlighted-color: @default-text-color-inverted; @search-term-highlighted-color: @default-text-color-inverted;
@search-term-drag-border-color: @base-gray; @search-term-drag-border-color: @base-gray;
@search-term-remove-action-bg: @default-remove-bg;
@search-term-remove-action-color: @default-remove-color;
@search-condition-remove-bg: @state-critical; @search-condition-remove-bg: @state-critical;
@search-condition-remove-color: @default-text-color-inverted; @search-condition-remove-color: @default-text-color-inverted;
@ -78,9 +87,7 @@
@search-editor-error-color: @state-critical; @search-editor-error-color: @state-critical;
@search-editor-control-color: @base-gray-light; @search-editor-control-color: @base-gray-light;
@search-editor-logical-op-bg: @base-gray-light; @search-editor-remove-control-color: @state-critical;
@search-editor-context-menu-border-color: @base-gray-light;
@search-editor-context-menu-bg: @default-bg;
@search-editor-drag-outline-color: @base-gray; @search-editor-drag-outline-color: @base-gray;
@control-color: @base-primary-color; @control-color: @base-primary-color;
@ -115,7 +122,7 @@
@schedule-element-fields-selected-bg: @primary-button-bg; @schedule-element-fields-selected-bg: @primary-button-bg;
@schedule-element-fields-selected-color: @default-text-color-inverted; @schedule-element-fields-selected-color: @default-text-color-inverted;
@schedule-element-fields-hover-bg: @base-primary-light; @schedule-element-fields-hover-bg: @base-primary-light;
@schedule-element-fields-outline-color: fade(@base-primary-bg, 50%); @schedule-element-fields-outline-color: @default-input-outline-color;
@schedule-element-fields-selected-outline-color: fade(#fff, 50%); @schedule-element-fields-selected-outline-color: fade(#fff, 50%);
@schedule-element-fields-selected-hover-bg: @primary-button-hover-bg; @schedule-element-fields-selected-hover-bg: @primary-button-hover-bg;
@schedule-element-fields-disabled-color: @base-gray; @schedule-element-fields-disabled-color: @base-gray;
@ -126,7 +133,7 @@
@empty-state-color: @base-gray-semilight; @empty-state-color: @base-gray-semilight;
@empty-state-bar-bg: @base-gray-lighter; @empty-state-bar-bg: @base-gray-lighter;
@list-item-title-hover-color: @base-primary-color; @list-item-title-hover-color: @link-hover-color;
@list-item-separation-bg: @base-gray-light; @list-item-separation-bg: @base-gray-light;
@iplWebLightRules: { @iplWebLightRules: {
@ -143,10 +150,17 @@
--default-text-color-light: fade(#535353, 75%); // --default-text-color --default-text-color-light: fade(#535353, 75%); // --default-text-color
--default-text-color-inverted: #F5F9FA; --default-text-color-inverted: #F5F9FA;
--default-input-bg: #DEECF1; --default-input-bg: #DEECF1;
--default-input-hover-bg: #C0CCCD;
--default-input-outline-color: @base-primary-light;
--default-remove-bg: var(--base-remove-bg);
--default-remove-color: var(--default-text-color-inverted);
--default-delete-bg: var(--base-remove-bg);
--default-delete-color: var(--default-text-color-inverted);
--primary-button-color: var(--default-text-color-inverted); --primary-button-color: var(--default-text-color-inverted);
--primary-button-bg: @primary-button-bg; --primary-button-bg: @primary-button-bg;
--primary-button-hover-bg: @primary-button-hover-bg; --primary-button-hover-bg: @primary-button-hover-bg;
--link-hover-color: var(--base-primary-color);
--searchbar-bg: var(--default-input-bg); --searchbar-bg: var(--default-input-bg);
--searchbar-scrollbar-bg: var(--base-gray-light); --searchbar-scrollbar-bg: var(--base-gray-light);
@ -162,6 +176,8 @@
--search-term-highlighted-bg: var(--primary-button-bg); --search-term-highlighted-bg: var(--primary-button-bg);
--search-term-highlighted-color: var(--default-text-color-inverted); --search-term-highlighted-color: var(--default-text-color-inverted);
--search-term-drag-border-color: var(--base-gray); --search-term-drag-border-color: var(--base-gray);
--search-term-remove-action-bg: var(--default-remove-bg);
--search-term-remove-action-color: var(--default-remove-color);
--search-condition-remove-bg: var(--base-remove-bg); --search-condition-remove-bg: var(--base-remove-bg);
--search-condition-remove-color: var(--default-text-color-inverted); --search-condition-remove-color: var(--default-text-color-inverted);
@ -171,9 +187,6 @@
--search-editor-error-color: var(--base-remove-bg); --search-editor-error-color: var(--base-remove-bg);
--search-editor-control-color: var(--base-gray-light); --search-editor-control-color: var(--base-gray-light);
--search-editor-logical-op-bg: var(--base-gray-light);
--search-editor-context-menu-border-color: var(--base-gray-light);
--search-editor-context-menu-bg: var(--default-text-color-inverted);
--search-editor-drag-outline-color: var(--base-gray); --search-editor-drag-outline-color: var(--base-gray);
--control-color: var(--primary-button-bg); --control-color: var(--primary-button-bg);
@ -203,7 +216,7 @@
--schedule-element-fields-selected-bg: var(--primary-button-bg); --schedule-element-fields-selected-bg: var(--primary-button-bg);
--schedule-element-fields-selected-color: var(--default-text-color-inverted); --schedule-element-fields-selected-color: var(--default-text-color-inverted);
--schedule-element-fields-hover-bg: @base-primary-light; --schedule-element-fields-hover-bg: @base-primary-light;
--schedule-element-fields-outline-color: fade(@base-primary-bg, 50%); --schedule-element-fields-outline-color: @default-input-outline-color;
--schedule-element-fields-selected-outline-color: fade(#fff, 50%); --schedule-element-fields-selected-outline-color: fade(#fff, 50%);
--schedule-element-fields-selected-hover-bg: var(--primary-button-hover-bg); --schedule-element-fields-selected-hover-bg: var(--primary-button-hover-bg);
--schedule-element-fields-disabled-color: var(--base-gray); --schedule-element-fields-disabled-color: var(--base-gray);
@ -214,7 +227,7 @@
--empty-state-color: var(--base-gray-semilight); --empty-state-color: var(--base-gray-semilight);
--empty-state-bar-bg: var(--base-gray-lighter); --empty-state-bar-bg: var(--base-gray-lighter);
--list-item-title-hover-color: var(--base-primary-color); --list-item-title-hover-color: var(--link-hover-color);
--list-item-separation-bg: var(--base-gray-light); --list-item-separation-bg: var(--base-gray-light);
} }
}; };

96
asset/js/iterator.js Normal file
View File

@ -0,0 +1,96 @@
(function (root, factory) {
"use strict";
if (typeof define === "function" && define.icinga) {
define(["exports"], factory);
} else {
factory(root.icingaIteratorPolyfill = root.icingaIteratorPolyfill || {});
}
}(self, function (exports) {
/**
* Polyfill for `Iterator.filter`
*
* @param {Symbol.iterator} iterator
* @param {function} callback
* @returns {Generator<*, void, *>}
*/
function* filter(iterator, callback) {
if (typeof iterator.filter === "function") {
yield* iterator.filter(callback);
}
for (const item of iterator) {
if (callback(item)) {
yield item;
}
}
}
/**
* Polyfill for `Iterator.find`
*
* @param {Symbol.iterator} iterator
* @param {function} callback
* @returns {*}
*/
function find(iterator, callback) {
if (typeof iterator.find === "function") {
return iterator.find(callback);
}
for (const item of iterator) {
if (callback(item)) {
return item;
}
}
}
/**
* Polyfill for `Iterator.map`
*
* @param {Symbol.iterator} iterator
* @param {function} callback
* @returns {Generator<*, void, *>}
*/
function* map(iterator, callback) {
if (typeof iterator.map === "function") {
yield* iterator.map(callback);
}
for (const item of iterator) {
yield callback(item);
}
}
/**
* Find the first key in the map whose value satisfies the provided testing function.
* @param {Map} map
* @param {function} callback Passed arguments are: value, key, map
* @returns {*} Returns undefined if no key satisfies the testing function.
*/
function findKey(map, callback) {
for (const key of findKeys(map, callback)) {
return key;
}
}
/**
* Find all keys in the map whose value satisfies the provided testing function.
* @param {Map} map
* @param {function} callback Passed arguments are: value, key, map
* @returns {Generator<*, void, *>}
*/
function* findKeys(map, callback) {
for (const [ key, value ] of map) {
if (callback(value, key, map)) {
yield key;
}
}
}
exports.findKeys = findKeys;
exports.findKey = findKey;
exports.filter = filter;
exports.find = find;
exports.map = map;
}));

View File

@ -632,7 +632,11 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
let eventData = { submittedBy: input }; let eventData = { submittedBy: input };
if (changeType === 'paste') { if (changeType === 'paste') {
// Ensure that what's pasted is also transmitted as value // Ensure that what's pasted is also transmitted as value
eventData['terms'] = this.termsToQueryString(data['terms']) + this.separator + data['input']; if (data['terms'].length === 0) {
eventData['terms'] = data['input'];
} else {
eventData['terms'] = this.termsToQueryString(data['terms']) + this.separator + data['input'];
}
} }
$(this.input.form).trigger('submit', eventData); $(this.input.form).trigger('submit', eventData);
@ -713,7 +717,14 @@ define(["../notjQuery", "Completer"], function ($, Completer) {
this.input.name = ''; this.input.name = '';
// Set the hidden input's value, it's what's sent // Set the hidden input's value, it's what's sent
if (event.detail && 'terms' in event.detail) { if (
event.detail
&& 'terms' in event.detail
&& (
! ('submittedBy' in event.detail)
|| event.detail.submittedBy === this.input
)
) {
this.termInput.value = event.detail.terms; this.termInput.value = event.detail.terms;
} else { } else {
let renderedTerms = this.termsToQueryString(this.usedTerms); let renderedTerms = this.termsToQueryString(this.usedTerms);

View File

@ -13,7 +13,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
* *
* @type {{}} * @type {{}}
*/ */
this.negationOperator = { label: '!', search: '!', class: 'logical_operator', type: 'negation_operator' }; this.negationOperator = { label: '!', search: '!', class: 'logical-operator', type: 'negation_operator' };
/** /**
* Supported grouping operators * Supported grouping operators
@ -21,8 +21,8 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
* @type {{close: {}, open: {}}} * @type {{close: {}, open: {}}}
*/ */
this.grouping_operators = { this.grouping_operators = {
open: { label: '(', search: '(', class: 'grouping_operator_open', type: 'grouping_operator' }, open: { label: '(', search: '(', class: 'grouping-operator-open', type: 'grouping_operator' },
close: { label: ')', search: ')', class: 'grouping_operator_close', type: 'grouping_operator' } close: { label: ')', search: ')', class: 'grouping-operator-close', type: 'grouping_operator' }
}; };
/** /**
@ -33,8 +33,8 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
* @type {{}[]} * @type {{}[]}
*/ */
this.logical_operators = [ this.logical_operators = [
{ label: '&', search: '&', class: 'logical_operator', type: 'logical_operator', default: true }, { label: '&', search: '&', class: 'logical-operator', type: 'logical_operator', default: true },
{ label: '|', search: '|', class: 'logical_operator', type: 'logical_operator' }, { label: '|', search: '|', class: 'logical-operator', type: 'logical_operator' },
]; ];
/** /**
@ -958,7 +958,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
label.dataset.type = termData.type; label.dataset.type = termData.type;
if (! termData.class) { if (! termData.class) {
label.classList.add(termData.type); label.classList.add(termData.type.replace('_', '-'));
} }
if (termData.counterpart >= 0) { if (termData.counterpart >= 0) {
@ -1256,11 +1256,11 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) {
let label = event.currentTarget; let label = event.currentTarget;
if (['column', 'operator', 'value'].includes(label.dataset.type)) { if (['column', 'operator', 'value'].includes(label.dataset.type)) {
// This adds a class to delay the remove button. If it's shown instantly upon hover // This adds an attr to delay the remove button. If it's shown instantly upon hover
// it's too easy to accidentally click it instead of the desired grouping operator. // it's too easy to accidentally click it instead of the desired grouping operator.
label.parentNode.classList.add('_hover_delay'); label.parentNode.dataset.hoverDelay = "";
setTimeout(function () { setTimeout(function () {
label.parentNode.classList.remove('_hover_delay'); delete label.parentNode.dataset.hoverDelay;
}, 500); }, 500);
} }

View File

@ -55,7 +55,7 @@ define(["../notjQuery", "../vendor/Sortable"], function ($, Sortable) {
if (! neighbour) { if (! neighbour) {
// User dropped the rule into an empty group // User dropped the rule into an empty group
placement = 'to'; placement = 'to';
neighbour = event.to.closest('[id]'); neighbour = event.to.closest('li[id]');
} }
} }

View File

@ -187,8 +187,16 @@ define(["../notjQuery", "../vendor/Sortable", "BaseInput"], function ($, Sortabl
const label = super.renderTerm(termData, termIndex); const label = super.renderTerm(termData, termIndex);
if (this.readOnly) { if (this.readOnly) {
const removeLabel = this.termContainer.dataset.removeActionLabel;
label.firstChild.readOnly = true; label.firstChild.readOnly = true;
label.appendChild($.render('<i class="icon fa-trash fa"></i>')); label.appendChild(
$.render(
`<div class="remove-action" title="${ removeLabel }">` +
'<i class="icon fa-trash fa"></i>' +
`<span class="remove-action-label">${ removeLabel }</span>` +
'</div>'
)
);
label.appendChild($.render('<span class="invalid-reason"></span>')); label.appendChild($.render('<span class="invalid-reason"></span>'));
} }
@ -311,7 +319,12 @@ define(["../notjQuery", "../vendor/Sortable", "BaseInput"], function ($, Sortabl
} }
onButtonClick(event) { onButtonClick(event) {
if (! this.hasSyntaxError()) { // Exchange terms only when an Enter key is pressed while not in the term input.
// If the pointerType is not empty, the click event is triggered by clicking the Submit button in the form,
// and the default submit event should not be prevented.
// The below solution does not work if the click event is triggered by pressing Space while on the Submit button.
// In which case the Submit button needs to be clicked again to trigger the form submission.
if (! this.hasSyntaxError() && event.pointerType === '') {
let addedTerms = this.exchangeTerm(); let addedTerms = this.exchangeTerm();
if (Object.keys(addedTerms).length) { if (Object.keys(addedTerms).length) {
this.togglePlaceholder(); this.togglePlaceholder();

View File

@ -19,4 +19,7 @@
<glyph unicode="&#xe909;" glyph-name="refresh-cert" d="M319.936 512h384v-257.088h-384v257.088zM383.936 544v96c0 35.328 28.672 64 64 64h128c35.328 0 64-28.672 64-64v-96h-256zM575.936 640h-128v-96h128v96zM192.448 235.136c68.864-103.168 186.368-171.136 319.552-171.136 141.568 0 265.344 76.8 331.904 190.912h142.4c-76.288-187.008-260.032-318.912-474.304-318.912-180.544 0-339.392 93.632-430.592 235.008l-81.536-47.104 2.112 324.096 281.472-160.384-91.008-52.48zM833.472 657.92c-68.544 104.832-187.008 174.080-321.472 174.080-167.040 0-309.376-106.944-362.112-256h-133.76c56.896 220.736 257.472 384 495.872 384 181.824 0 341.632-94.976 432.448-237.952l79.68 45.952-2.112-324.096-281.472 160.384 92.928 53.632z" /> <glyph unicode="&#xe909;" glyph-name="refresh-cert" d="M319.936 512h384v-257.088h-384v257.088zM383.936 544v96c0 35.328 28.672 64 64 64h128c35.328 0 64-28.672 64-64v-96h-256zM575.936 640h-128v-96h128v96zM192.448 235.136c68.864-103.168 186.368-171.136 319.552-171.136 141.568 0 265.344 76.8 331.904 190.912h142.4c-76.288-187.008-260.032-318.912-474.304-318.912-180.544 0-339.392 93.632-430.592 235.008l-81.536-47.104 2.112 324.096 281.472-160.384-91.008-52.48zM833.472 657.92c-68.544 104.832-187.008 174.080-321.472 174.080-167.040 0-309.376-106.944-362.112-256h-133.76c56.896 220.736 257.472 384 495.872 384 181.824 0 341.632-94.976 432.448-237.952l79.68 45.952-2.112-324.096-281.472 160.384 92.928 53.632z" />
<glyph unicode="&#xe90a;" glyph-name="th-list" d="M64.491 172.602c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h30.6c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-30.6c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM64.491 448c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h30.6c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-30.6c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM64.491 723.398c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h30.6c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-30.6c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM288.252 172.602c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h534.54c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-534.54c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM288.252 448c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h534.54c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-534.54c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM288.252 723.398c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h534.54c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-534.54c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768z" /> <glyph unicode="&#xe90a;" glyph-name="th-list" d="M64.491 172.602c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h30.6c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-30.6c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM64.491 448c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h30.6c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-30.6c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM64.491 723.398c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h30.6c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-30.6c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM288.252 172.602c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h534.54c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-534.54c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM288.252 448c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h534.54c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-534.54c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768zM288.252 723.398c0-19.125 6.694-35.381 20.081-48.768s29.644-20.081 48.768-20.081h534.54c19.125 0 35.381 6.694 48.768 20.081s20.081 29.644 20.081 48.768c0 19.125-6.694 35.381-20.081 48.768s-29.644 20.081-48.768 20.081h-534.54c-19.125 0-35.381-6.694-48.768-20.081s-20.081-29.644-20.081-48.768z" />
<glyph unicode="&#xe90b;" glyph-name="filter-check-circle" d="M7.232 869.504c11.776 24.896 36.672 40.704 64.192 40.704h768c27.584 0 52.48-15.808 64.192-40.704s8.192-54.208-9.216-75.52l-189.376-231.488c-142.592-29.312-249.6-155.392-249.6-306.496 0-48.704 11.2-94.912 31.104-136-2.816 1.408-5.696 3.2-8.32 5.184l-113.792 85.312c-14.4 10.688-22.784 27.52-22.784 45.504v140.608l-325.312 397.504c-17.216 21.184-20.992 50.688-9.088 75.392zM1024 254.976c-2.176-72.512-27.456-132.992-75.84-181.504-48.384-48.576-108.672-73.344-180.992-74.496-72.256 1.152-132.288 25.92-180.096 74.496-47.808 48.512-72.832 108.992-75.072 181.504 2.24 72.512 27.264 133.056 75.072 181.568s107.84 73.344 180.096 74.432c72.32-1.088 132.608-25.92 180.992-74.432s73.664-109.056 75.84-181.568zM966.144 378.752c-0.512 0.704-1.088 1.344-1.728 1.92-22.656 22.72-59.52 22.72-82.176 0l-80.448-80.32-33.024-76.608c0 0-25.6 65.728-25.6 65.728l-29.312 29.312c-22.656 22.656-59.52 22.656-82.176 0-22.72-22.72-22.72-59.52 0-82.24l94.272-94.272c11.2-11.2 25.856-16.832 40.512-16.96 0.448 0 0.896 0 1.28 0 14.656 0.192 29.248 5.824 40.384 16.96l156.224 156.224c22.016 22.016 22.656 57.472 1.792 80.256z" /> <glyph unicode="&#xe90b;" glyph-name="filter-check-circle" d="M7.232 869.504c11.776 24.896 36.672 40.704 64.192 40.704h768c27.584 0 52.48-15.808 64.192-40.704s8.192-54.208-9.216-75.52l-189.376-231.488c-142.592-29.312-249.6-155.392-249.6-306.496 0-48.704 11.2-94.912 31.104-136-2.816 1.408-5.696 3.2-8.32 5.184l-113.792 85.312c-14.4 10.688-22.784 27.52-22.784 45.504v140.608l-325.312 397.504c-17.216 21.184-20.992 50.688-9.088 75.392zM1024 254.976c-2.176-72.512-27.456-132.992-75.84-181.504-48.384-48.576-108.672-73.344-180.992-74.496-72.256 1.152-132.288 25.92-180.096 74.496-47.808 48.512-72.832 108.992-75.072 181.504 2.24 72.512 27.264 133.056 75.072 181.568s107.84 73.344 180.096 74.432c72.32-1.088 132.608-25.92 180.992-74.432s73.664-109.056 75.84-181.568zM966.144 378.752c-0.512 0.704-1.088 1.344-1.728 1.92-22.656 22.72-59.52 22.72-82.176 0l-80.448-80.32-33.024-76.608c0 0-25.6 65.728-25.6 65.728l-29.312 29.312c-22.656 22.656-59.52 22.656-82.176 0-22.72-22.72-22.72-59.52 0-82.24l94.272-94.272c11.2-11.2 25.856-16.832 40.512-16.96 0.448 0 0.896 0 1.28 0 14.656 0.192 29.248 5.824 40.384 16.96l156.224 156.224c22.016 22.016 22.656 57.472 1.792 80.256z" />
<glyph unicode="&#xe90c;" glyph-name="add-inside" d="M561.28 495.36v160.64h-99.2v-160.64h-158.080v-99.2h158.080v-156.16h99.2v156.16h158.72v99.2h-158.72zM512 821.12c206.080 0 373.12-167.68 373.12-373.12s-167.68-373.12-373.12-373.12-373.12 167.68-373.12 373.12 167.68 373.12 373.12 373.12zM512 896c-247.68 0-448-200.32-448-448s200.32-448 448-448 448 200.32 448 448-200.32 448-448 448v0z" />
<glyph unicode="&#xe90d;" glyph-name="insert-group" d="M419.2 460.16h-156.8c-10.88 0-19.2 8.96-19.2 19.2v288.64h-115.2v-307.84c0-74.24 60.16-135.040 135.040-135.040h156.8l-50.56-173.44 231.040 173.44s76.8 57.6 76.8 57.6l-308.48 273.28 51.2-195.84zM64 896h448v-74.88h-448v74.88zM853.12 151.68v464c0 93.44 75.52 149.12 149.12 149.12-45.44 0-99.84 1.28-149.12 1.28-103.040 0-149.12-75.52-149.12-150.4v-464c0-74.88 46.080-151.68 149.12-151.68 49.28 0 103.68 0.64 149.12 1.28-73.6 0-149.76 76.16-149.12 150.4z" />
<glyph unicode="&#xe90e;" glyph-name="wrap" d="M802.56 897.28c-49.28 0-110.72-0.64-156.16-1.28 73.6 0 168.32-60.16 167.68-134.4v-627.2c0-74.24-94.72-134.4-167.68-134.4 45.44 0 106.88-1.28 156.16-1.28 103.040 0 157.44 60.8 157.44 135.68v627.2c0 74.88-55.040 135.68-157.44 135.68zM704 457.6h-152.96v182.4h-78.080v-182.4h-152.96v-77.44h152.96v-188.16h78.080v188.16h152.96v77.44zM209.92 134.4v627.2c0 74.24 94.72 134.4 167.68 134.4-45.44 0-106.88 1.28-156.16 1.28-102.4 0-157.44-60.8-157.44-135.68v-627.2c0-74.88 55.040-135.68 157.44-135.68 49.28 0 110.72 0.64 156.16 1.28-73.6 0-168.32 60.16-167.68 134.4z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8.77 7.26v-2.51h-1.55v2.51h-2.47v1.55h2.47v2.44h1.55v-2.44h2.48v-1.55z"/><path d="m8 2.17c3.22 0 5.83 2.62 5.83 5.83s-2.62 5.83-5.83 5.83-5.83-2.62-5.83-5.83 2.62-5.83 5.83-5.83m0-1.17c-3.87 0-7 3.13-7 7s3.13 7 7 7 7-3.13 7-7-3.13-7-7-7z"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m6.55 7.81h-2.45c-.17 0-.3-.14-.3-.3v-4.51h-1.8v4.81c0 1.16.94 2.11 2.11 2.11h2.45l-.79 2.71 3.61-2.71s1.2-.9 1.2-.9l-4.82-4.27.8 3.06z" fill-rule="evenodd"/><path d="m1 1h7v1.17h-7z"/><path d="m13.33 12.63v-7.25c0-1.46 1.18-2.33 2.33-2.33-.71 0-1.56-.02-2.33-.02-1.61 0-2.33 1.18-2.33 2.35v7.25c0 1.17.72 2.37 2.33 2.37.77 0 1.62-.01 2.33-.02-1.15 0-2.34-1.19-2.33-2.35z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="m12.54.98c-.77 0-1.73.01-2.44.02 1.15 0 2.63.94 2.62 2.1v9.8c0 1.16-1.48 2.1-2.62 2.1.71 0 1.67.02 2.44.02 1.61 0 2.46-.95 2.46-2.12v-9.8c0-1.17-.86-2.12-2.46-2.12z"/><path d="m11 7.85h-2.39v-2.85h-1.22v2.85h-2.39v1.21h2.39v2.94h1.22v-2.94h2.39z"/><path d="m3.28 12.9v-9.8c0-1.16 1.48-2.1 2.62-2.1-.71 0-1.67-.02-2.44-.02-1.6 0-2.46.95-2.46 2.12v9.8c0 1.17.86 2.12 2.46 2.12.77 0 1.73-.01 2.44-.02-1.15 0-2.63-.94-2.62-2.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -24,6 +24,15 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.iicon-add-inside:before {
content: "\e90c";
}
.iicon-insert-group:before {
content: "\e90d";
}
.iicon-wrap:before {
content: "\e90e";
}
.iicon-certificate:before { .iicon-certificate:before {
content: "\e906"; content: "\e906";
} }

630
composer.lock generated

File diff suppressed because it is too large Load Diff

7
vendor/autoload.php vendored
View File

@ -14,12 +14,9 @@ if (PHP_VERSION_ID < 50600) {
echo $err; echo $err;
} }
} }
trigger_error( throw new RuntimeException($err);
$err,
E_USER_ERROR
);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit20d4022bc196691807f55d4a47c06474::getLoader(); return ComposerAutoloaderInit7a1692c86b6fc70eaaf43c4bee3673aa::getLoader();

View File

@ -5,22 +5,26 @@
"keywords": [ "keywords": [
"Brick", "Brick",
"Math", "Math",
"Mathematics",
"Arbitrary-precision", "Arbitrary-precision",
"Arithmetic", "Arithmetic",
"BigInteger", "BigInteger",
"BigDecimal", "BigDecimal",
"BigRational", "BigRational",
"Bignum" "BigNumber",
"Bignum",
"Decimal",
"Rational",
"Integer"
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^7.1 || ^8.0", "php": "^8.2"
"ext-json": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", "phpunit/phpunit": "^11.5",
"php-coveralls/php-coveralls": "^2.2", "php-coveralls/php-coveralls": "^2.2",
"vimeo/psalm": "4.9.2" "phpstan/phpstan": "2.1.22"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -8,13 +8,13 @@ use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException; use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException; use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Brick\Math\Internal\CalculatorRegistry;
use Override;
/** /**
* Immutable, arbitrary-precision signed decimal numbers. * Immutable, arbitrary-precision signed decimal numbers.
*
* @psalm-immutable
*/ */
final class BigDecimal extends BigNumber final readonly class BigDecimal extends BigNumber
{ {
/** /**
* The unscaled value of this decimal number. * The unscaled value of this decimal number.
@ -22,25 +22,23 @@ final class BigDecimal extends BigNumber
* This is a string of digits with an optional leading minus sign. * This is a string of digits with an optional leading minus sign.
* No leading zero must be present. * No leading zero must be present.
* No leading minus sign must be present if the value is 0. * No leading minus sign must be present if the value is 0.
*
* @var string
*/ */
private $value; private string $value;
/** /**
* The scale (number of digits after the decimal point) of this decimal number. * The scale (number of digits after the decimal point) of this decimal number.
* *
* This must be zero or more. * This must be zero or more.
*
* @var int
*/ */
private $scale; private int $scale;
/** /**
* Protected constructor. Use a factory method to obtain an instance. * Protected constructor. Use a factory method to obtain an instance.
* *
* @param string $value The unscaled value, validated. * @param string $value The unscaled value, validated.
* @param int $scale The scale, validated. * @param int $scale The scale, validated.
*
* @pure
*/ */
protected function __construct(string $value, int $scale = 0) protected function __construct(string $value, int $scale = 0)
{ {
@ -48,20 +46,10 @@ final class BigDecimal extends BigNumber
$this->scale = $scale; $this->scale = $scale;
} }
/** #[Override]
* Creates a BigDecimal of the given value. protected static function from(BigNumber $number): static
*
* @param BigNumber|int|float|string $value
*
* @return BigDecimal
*
* @throws MathException If the value cannot be converted to a BigDecimal.
*
* @psalm-pure
*/
public static function of($value) : BigNumber
{ {
return parent::of($value)->toBigDecimal(); return $number->toBigDecimal();
} }
/** /**
@ -70,36 +58,33 @@ final class BigDecimal extends BigNumber
* Example: `(12345, 3)` will result in the BigDecimal `12.345`. * Example: `(12345, 3)` will result in the BigDecimal `12.345`.
* *
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number, positive or zero. * @param int $scale The scale of the number. If negative, the scale will be set to zero
* and the unscaled value will be adjusted accordingly.
* *
* @return BigDecimal * @pure
*
* @throws \InvalidArgumentException If the scale is negative.
*
* @psalm-pure
*/ */
public static function ofUnscaledValue($value, int $scale = 0) : BigDecimal public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal
{ {
$value = (string) BigInteger::of($value);
if ($scale < 0) { if ($scale < 0) {
throw new \InvalidArgumentException('The scale cannot be negative.'); if ($value !== '0') {
$value .= \str_repeat('0', -$scale);
}
$scale = 0;
} }
return new BigDecimal((string) BigInteger::of($value), $scale); return new BigDecimal($value, $scale);
} }
/** /**
* Returns a BigDecimal representing zero, with a scale of zero. * Returns a BigDecimal representing zero, with a scale of zero.
* *
* @return BigDecimal * @pure
*
* @psalm-pure
*/ */
public static function zero() : BigDecimal public static function zero() : BigDecimal
{ {
/** /** @var BigDecimal|null $zero */
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $zero
*/
static $zero; static $zero;
if ($zero === null) { if ($zero === null) {
@ -112,16 +97,11 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a BigDecimal representing one, with a scale of zero. * Returns a BigDecimal representing one, with a scale of zero.
* *
* @return BigDecimal * @pure
*
* @psalm-pure
*/ */
public static function one() : BigDecimal public static function one() : BigDecimal
{ {
/** /** @var BigDecimal|null $one */
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $one
*/
static $one; static $one;
if ($one === null) { if ($one === null) {
@ -134,16 +114,11 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a BigDecimal representing ten, with a scale of zero. * Returns a BigDecimal representing ten, with a scale of zero.
* *
* @return BigDecimal * @pure
*
* @psalm-pure
*/ */
public static function ten() : BigDecimal public static function ten() : BigDecimal
{ {
/** /** @var BigDecimal|null $ten */
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $ten
*/
static $ten; static $ten;
if ($ten === null) { if ($ten === null) {
@ -160,11 +135,11 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
* *
* @return BigDecimal The result.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal. * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/ */
public function plus($that) : BigDecimal public function plus(BigNumber|int|float|string $that) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -178,7 +153,7 @@ final class BigDecimal extends BigNumber
[$a, $b] = $this->scaleValues($this, $that); [$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->add($a, $b); $value = CalculatorRegistry::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
@ -191,11 +166,11 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
* *
* @return BigDecimal The result.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal. * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/ */
public function minus($that) : BigDecimal public function minus(BigNumber|int|float|string $that) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -205,7 +180,7 @@ final class BigDecimal extends BigNumber
[$a, $b] = $this->scaleValues($this, $that); [$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->sub($a, $b); $value = CalculatorRegistry::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
@ -218,11 +193,11 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
* *
* @return BigDecimal The result.
*
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal. * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
*
* @pure
*/ */
public function multipliedBy($that) : BigDecimal public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -234,7 +209,7 @@ final class BigDecimal extends BigNumber
return $that; return $that;
} }
$value = Calculator::get()->mul($this->value, $that->value); $value = CalculatorRegistry::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale; $scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
@ -245,14 +220,14 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. * @param BigNumber|int|float|string $that The divisor.
* @param int|null $scale The desired scale, or null to use the scale of this number. * @param int|null $scale The desired scale, or null to use the scale of this number.
* @param int $roundingMode An optional rounding mode. * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @return BigDecimal
* *
* @throws \InvalidArgumentException If the scale or rounding mode is invalid. * @throws \InvalidArgumentException If the scale or rounding mode is invalid.
* @throws MathException If the number is invalid, is zero, or rounding was necessary. * @throws MathException If the number is invalid, is zero, or rounding was necessary.
*
* @pure
*/ */
public function dividedBy($that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -273,7 +248,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale + $scale); $p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale); $q = $that->valueWithMinScale($this->scale - $scale);
$result = Calculator::get()->divRound($p, $q, $roundingMode); $result = CalculatorRegistry::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale); return new BigDecimal($result, $scale);
} }
@ -285,12 +260,12 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @return BigDecimal The result.
*
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero, * @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
* or the result yields an infinite number of digits. * or the result yields an infinite number of digits.
*
* @pure
*/ */
public function exactlyDividedBy($that) : BigDecimal public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -303,7 +278,7 @@ final class BigDecimal extends BigNumber
$d = \rtrim($b, '0'); $d = \rtrim($b, '0');
$scale = \strlen($b) - \strlen($d); $scale = \strlen($b) - \strlen($d);
$calculator = Calculator::get(); $calculator = CalculatorRegistry::get();
foreach ([5, 2] as $prime) { foreach ([5, 2] as $prime) {
for (;;) { for (;;) {
@ -321,16 +296,36 @@ final class BigDecimal extends BigNumber
return $this->dividedBy($that, $scale)->stripTrailingZeros(); return $this->dividedBy($that, $scale)->stripTrailingZeros();
} }
/**
* Limits (clamps) this number between the given minimum and maximum values.
*
* If the number is lower than $min, returns a copy of $min.
* If the number is greater than $max, returns a copy of $max.
* Otherwise, returns this number unchanged.
*
* @param BigNumber|int|float|string $min The minimum. Must be convertible to a BigDecimal.
* @param BigNumber|int|float|string $max The maximum. Must be convertible to a BigDecimal.
*
* @throws MathException If min/max are not convertible to a BigDecimal.
*/
public function clamp(BigNumber|int|float|string $min, BigNumber|int|float|string $max) : BigDecimal
{
if ($this->isLessThan($min)) {
return BigDecimal::of($min);
} elseif ($this->isGreaterThan($max)) {
return BigDecimal::of($max);
}
return $this;
}
/** /**
* Returns this number exponentiated to the given value. * Returns this number exponentiated to the given value.
* *
* The result has a scale of `$this->scale * $exponent`. * The result has a scale of `$this->scale * $exponent`.
* *
* @param int $exponent The exponent.
*
* @return BigDecimal The result.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/ */
public function power(int $exponent) : BigDecimal public function power(int $exponent) : BigDecimal
{ {
@ -350,21 +345,21 @@ final class BigDecimal extends BigNumber
)); ));
} }
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent); return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
} }
/** /**
* Returns the quotient of the division of this number by this given one. * Returns the quotient of the division of this number by the given one.
* *
* The quotient has a scale of `0`. * The quotient has a scale of `0`.
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @return BigDecimal The quotient.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero. * @throws MathException If the divisor is not a valid decimal number, or is zero.
*
* @pure
*/ */
public function quotient($that) : BigDecimal public function quotient(BigNumber|int|float|string $that) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -375,23 +370,23 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale); $p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale); $q = $that->valueWithMinScale($this->scale);
$quotient = Calculator::get()->divQ($p, $q); $quotient = CalculatorRegistry::get()->divQ($p, $q);
return new BigDecimal($quotient, 0); return new BigDecimal($quotient, 0);
} }
/** /**
* Returns the remainder of the division of this number by this given one. * Returns the remainder of the division of this number by the given one.
* *
* The remainder has a scale of `max($this->scale, $that->scale)`. * The remainder has a scale of `max($this->scale, $that->scale)`.
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @return BigDecimal The remainder.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero. * @throws MathException If the divisor is not a valid decimal number, or is zero.
*
* @pure
*/ */
public function remainder($that) : BigDecimal public function remainder(BigNumber|int|float|string $that) : BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -402,7 +397,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale); $p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale); $q = $that->valueWithMinScale($this->scale);
$remainder = Calculator::get()->divR($p, $q); $remainder = CalculatorRegistry::get()->divR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
@ -416,11 +411,13 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @return BigDecimal[] An array containing the quotient and the remainder. * @return array{BigDecimal, BigDecimal} An array containing the quotient and the remainder.
* *
* @throws MathException If the divisor is not a valid decimal number, or is zero. * @throws MathException If the divisor is not a valid decimal number, or is zero.
*
* @pure
*/ */
public function quotientAndRemainder($that) : array public function quotientAndRemainder(BigNumber|int|float|string $that) : array
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@ -431,7 +428,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale); $p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale); $q = $that->valueWithMinScale($this->scale);
[$quotient, $remainder] = Calculator::get()->divQR($p, $q); [$quotient, $remainder] = CalculatorRegistry::get()->divQR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
@ -444,12 +441,10 @@ final class BigDecimal extends BigNumber
/** /**
* Returns the square root of this number, rounded down to the given number of decimals. * Returns the square root of this number, rounded down to the given number of decimals.
* *
* @param int $scale
*
* @return BigDecimal
*
* @throws \InvalidArgumentException If the scale is negative. * @throws \InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative. * @throws NegativeNumberException If this number is negative.
*
* @pure
*/ */
public function sqrt(int $scale) : BigDecimal public function sqrt(int $scale) : BigDecimal
{ {
@ -481,7 +476,7 @@ final class BigDecimal extends BigNumber
$value = \substr($value, 0, $addDigits); $value = \substr($value, 0, $addDigits);
} }
$value = Calculator::get()->sqrt($value); $value = CalculatorRegistry::get()->sqrt($value);
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
} }
@ -489,9 +484,7 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left. * Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
* *
* @param int $n * @pure
*
* @return BigDecimal
*/ */
public function withPointMovedLeft(int $n) : BigDecimal public function withPointMovedLeft(int $n) : BigDecimal
{ {
@ -509,9 +502,7 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right. * Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
* *
* @param int $n * @pure
*
* @return BigDecimal
*/ */
public function withPointMovedRight(int $n) : BigDecimal public function withPointMovedRight(int $n) : BigDecimal
{ {
@ -539,7 +530,7 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part. * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
* *
* @return BigDecimal * @pure
*/ */
public function stripTrailingZeros() : BigDecimal public function stripTrailingZeros() : BigDecimal
{ {
@ -572,7 +563,7 @@ final class BigDecimal extends BigNumber
/** /**
* Returns the absolute value of this number. * Returns the absolute value of this number.
* *
* @return BigDecimal * @pure
*/ */
public function abs() : BigDecimal public function abs() : BigDecimal
{ {
@ -582,17 +573,15 @@ final class BigDecimal extends BigNumber
/** /**
* Returns the negated value of this number. * Returns the negated value of this number.
* *
* @return BigDecimal * @pure
*/ */
public function negated() : BigDecimal public function negated() : BigDecimal
{ {
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale); return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
} }
/** #[Override]
* {@inheritdoc} public function compareTo(BigNumber|int|float|string $that) : int
*/
public function compareTo($that) : int
{ {
$that = BigNumber::of($that); $that = BigNumber::of($that);
@ -603,42 +592,69 @@ final class BigDecimal extends BigNumber
if ($that instanceof BigDecimal) { if ($that instanceof BigDecimal) {
[$a, $b] = $this->scaleValues($this, $that); [$a, $b] = $this->scaleValues($this, $that);
return Calculator::get()->cmp($a, $b); return CalculatorRegistry::get()->cmp($a, $b);
} }
return - $that->compareTo($this); return - $that->compareTo($this);
} }
/** #[Override]
* {@inheritdoc}
*/
public function getSign() : int public function getSign() : int
{ {
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
} }
/** /**
* @return BigInteger * @pure
*/ */
public function getUnscaledValue() : BigInteger public function getUnscaledValue() : BigInteger
{ {
return BigInteger::create($this->value); return self::newBigInteger($this->value);
} }
/** /**
* @return int * @pure
*/ */
public function getScale() : int public function getScale() : int
{ {
return $this->scale; return $this->scale;
} }
/**
* Returns the number of significant digits in the number.
*
* This is the number of digits to both sides of the decimal point, stripped of leading zeros.
* The sign has no impact on the result.
*
* Examples:
* 0 => 0
* 0.0 => 0
* 123 => 3
* 123.456 => 6
* 0.00123 => 3
* 0.0012300 => 5
*
* @pure
*/
public function getPrecision(): int
{
$value = $this->value;
if ($value === '0') {
return 0;
}
$length = \strlen($value);
return ($value[0] === '-') ? $length - 1 : $length;
}
/** /**
* Returns a string representing the integral part of this decimal number. * Returns a string representing the integral part of this decimal number.
* *
* Example: `-123.456` => `-123`. * Example: `-123.456` => `-123`.
* *
* @return string * @pure
*/ */
public function getIntegralPart() : string public function getIntegralPart() : string
{ {
@ -658,7 +674,7 @@ final class BigDecimal extends BigNumber
* *
* Examples: `-123.456` => '456', `123` => ''. * Examples: `-123.456` => '456', `123` => ''.
* *
* @return string * @pure
*/ */
public function getFractionalPart() : string public function getFractionalPart() : string
{ {
@ -674,46 +690,38 @@ final class BigDecimal extends BigNumber
/** /**
* Returns whether this decimal number has a non-zero fractional part. * Returns whether this decimal number has a non-zero fractional part.
* *
* @return bool * @pure
*/ */
public function hasNonZeroFractionalPart() : bool public function hasNonZeroFractionalPart() : bool
{ {
return $this->getFractionalPart() !== \str_repeat('0', $this->scale); return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigInteger() : BigInteger public function toBigInteger() : BigInteger
{ {
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0); $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return BigInteger::create($zeroScaleDecimal->value); return self::newBigInteger($zeroScaleDecimal->value);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigDecimal() : BigDecimal public function toBigDecimal() : BigDecimal
{ {
return $this; return $this;
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigRational() : BigRational public function toBigRational() : BigRational
{ {
$numerator = BigInteger::create($this->value); $numerator = self::newBigInteger($this->value);
$denominator = BigInteger::create('1' . \str_repeat('0', $this->scale)); $denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
return BigRational::create($numerator, $denominator, false); return self::newBigRational($numerator, $denominator, false);
} }
/** #[Override]
* {@inheritdoc} public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
*/
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{ {
if ($scale === $this->scale) { if ($scale === $this->scale) {
return $this; return $this;
@ -722,33 +730,32 @@ final class BigDecimal extends BigNumber
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode); return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toInt() : int public function toInt() : int
{ {
return $this->toBigInteger()->toInt(); return $this->toBigInteger()->toInt();
} }
/** #[Override]
* {@inheritdoc}
*/
public function toFloat() : float public function toFloat() : float
{ {
return (float) (string) $this; return (float) (string) $this;
} }
/** /**
* {@inheritdoc} * @return numeric-string
*/ */
#[Override]
public function __toString() : string public function __toString() : string
{ {
if ($this->scale === 0) { if ($this->scale === 0) {
/** @var numeric-string */
return $this->value; return $this->value;
} }
$value = $this->getUnscaledValueWithLeadingZeros(); $value = $this->getUnscaledValueWithLeadingZeros();
/** @phpstan-ignore return.type */
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale); return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
} }
@ -768,67 +775,29 @@ final class BigDecimal extends BigNumber
* This method is only here to allow unserializing the object and cannot be accessed directly. * This method is only here to allow unserializing the object and cannot be accessed directly.
* *
* @internal * @internal
* @psalm-suppress RedundantPropertyInitializationCheck
* *
* @param array{value: string, scale: int} $data * @param array{value: string, scale: int} $data
* *
* @return void
*
* @throws \LogicException * @throws \LogicException
*/ */
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->value)) { if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
} }
/** @phpstan-ignore deadCode.unreachable */
$this->value = $data['value']; $this->value = $data['value'];
$this->scale = $data['scale']; $this->scale = $data['scale'];
} }
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*
* @return string
*/
public function serialize() : string
{
return $this->value . ':' . $this->scale;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param string $value
*
* @return void
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->value)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$value, $scale] = \explode(':', $value);
$this->value = $value;
$this->scale = (int) $scale;
}
/** /**
* Puts the internal values of the given decimal numbers on the same scale. * Puts the internal values of the given decimal numbers on the same scale.
* *
* @param BigDecimal $x The first decimal number.
* @param BigDecimal $y The second decimal number.
*
* @return array{string, string} The scaled integer values of $x and $y. * @return array{string, string} The scaled integer values of $x and $y.
*
* @pure
*/ */
private function scaleValues(BigDecimal $x, BigDecimal $y) : array private function scaleValues(BigDecimal $x, BigDecimal $y) : array
{ {
@ -845,9 +814,7 @@ final class BigDecimal extends BigNumber
} }
/** /**
* @param int $scale * @pure
*
* @return string
*/ */
private function valueWithMinScale(int $scale) : string private function valueWithMinScale(int $scale) : string
{ {
@ -863,7 +830,7 @@ final class BigDecimal extends BigNumber
/** /**
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number. * Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
* *
* @return string * @pure
*/ */
private function getUnscaledValueWithLeadingZeros() : string private function getUnscaledValueWithLeadingZeros() : string
{ {

View File

@ -10,51 +10,41 @@ use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException; use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Brick\Math\Internal\CalculatorRegistry;
use Override;
/** /**
* An arbitrary-size integer. * An arbitrary-size integer.
* *
* All methods accepting a number as a parameter accept either a BigInteger instance, * All methods accepting a number as a parameter accept either a BigInteger instance,
* an integer, or a string representing an arbitrary size integer. * an integer, or a string representing an arbitrary size integer.
*
* @psalm-immutable
*/ */
final class BigInteger extends BigNumber final readonly class BigInteger extends BigNumber
{ {
/** /**
* The value, as a string of digits with optional leading minus sign. * The value, as a string of digits with optional leading minus sign.
* *
* No leading zeros must be present. * No leading zeros must be present.
* No leading minus sign must be present if the number is zero. * No leading minus sign must be present if the number is zero.
*
* @var string
*/ */
private $value; private string $value;
/** /**
* Protected constructor. Use a factory method to obtain an instance. * Protected constructor. Use a factory method to obtain an instance.
* *
* @param string $value A string of digits, with optional leading minus sign. * @param string $value A string of digits, with optional leading minus sign.
*
* @pure
*/ */
protected function __construct(string $value) protected function __construct(string $value)
{ {
$this->value = $value; $this->value = $value;
} }
/** #[Override]
* Creates a BigInteger of the given value. protected static function from(BigNumber $number): static
*
* @param BigNumber|int|float|string $value
*
* @return BigInteger
*
* @throws MathException If the value cannot be converted to a BigInteger.
*
* @psalm-pure
*/
public static function of($value) : BigNumber
{ {
return parent::of($value)->toBigInteger(); return $number->toBigInteger();
} }
/** /**
@ -71,12 +61,10 @@ final class BigInteger extends BigNumber
* @param string $number The number to convert, in the given base. * @param string $number The number to convert, in the given base.
* @param int $base The base of the number, between 2 and 36. * @param int $base The base of the number, between 2 and 36.
* *
* @return BigInteger
*
* @throws NumberFormatException If the number is empty, or contains invalid chars for the given base. * @throws NumberFormatException If the number is empty, or contains invalid chars for the given base.
* @throws \InvalidArgumentException If the base is out of range. * @throws \InvalidArgumentException If the base is out of range.
* *
* @psalm-pure * @pure
*/ */
public static function fromBase(string $number, int $base) : BigInteger public static function fromBase(string $number, int $base) : BigInteger
{ {
@ -125,7 +113,7 @@ final class BigInteger extends BigNumber
return new BigInteger($sign . $number); return new BigInteger($sign . $number);
} }
$result = Calculator::get()->fromBase($number, $base); $result = CalculatorRegistry::get()->fromBase($number, $base);
return new BigInteger($sign . $result); return new BigInteger($sign . $result);
} }
@ -138,12 +126,10 @@ final class BigInteger extends BigNumber
* @param string $number The number to parse. * @param string $number The number to parse.
* @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8. * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8.
* *
* @return BigInteger
*
* @throws NumberFormatException If the given number is empty or contains invalid chars for the given alphabet. * @throws NumberFormatException If the given number is empty or contains invalid chars for the given alphabet.
* @throws \InvalidArgumentException If the alphabet does not contain at least 2 chars. * @throws \InvalidArgumentException If the alphabet does not contain at least 2 chars.
* *
* @psalm-pure * @pure
*/ */
public static function fromArbitraryBase(string $number, string $alphabet) : BigInteger public static function fromArbitraryBase(string $number, string $alphabet) : BigInteger
{ {
@ -163,7 +149,7 @@ final class BigInteger extends BigNumber
throw NumberFormatException::charNotInAlphabet($matches[0]); throw NumberFormatException::charNotInAlphabet($matches[0]);
} }
$number = Calculator::get()->fromArbitraryBase($number, $alphabet, $base); $number = CalculatorRegistry::get()->fromArbitraryBase($number, $alphabet, $base);
return new BigInteger($number); return new BigInteger($number);
} }
@ -183,9 +169,9 @@ final class BigInteger extends BigNumber
* @param bool $signed Whether to interpret as a signed number in two's-complement representation with a leading * @param bool $signed Whether to interpret as a signed number in two's-complement representation with a leading
* sign bit. * sign bit.
* *
* @return BigInteger
*
* @throws NumberFormatException If the string is empty. * @throws NumberFormatException If the string is empty.
*
* @pure
*/ */
public static function fromBytes(string $value, bool $signed = true) : BigInteger public static function fromBytes(string $value, bool $signed = true) : BigInteger
{ {
@ -217,14 +203,10 @@ final class BigInteger extends BigNumber
* *
* Using the default random bytes generator, this method is suitable for cryptographic use. * Using the default random bytes generator, this method is suitable for cryptographic use.
* *
* @psalm-param callable(int): string $randomBytesGenerator * @param int $numBits The number of bits.
* * @param (callable(int): string)|null $randomBytesGenerator A function that accepts a number of bytes, and returns
* @param int $numBits The number of bits. * a string of random bytes of the given length. Defaults
* @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, and returns a * to the `random_bytes()` function.
* string of random bytes of the given length. Defaults to the
* `random_bytes()` function.
*
* @return BigInteger
* *
* @throws \InvalidArgumentException If $numBits is negative. * @throws \InvalidArgumentException If $numBits is negative.
*/ */
@ -239,9 +221,10 @@ final class BigInteger extends BigNumber
} }
if ($randomBytesGenerator === null) { if ($randomBytesGenerator === null) {
$randomBytesGenerator = 'random_bytes'; $randomBytesGenerator = random_bytes(...);
} }
/** @var int<1, max> $byteLength */
$byteLength = \intdiv($numBits - 1, 8) + 1; $byteLength = \intdiv($numBits - 1, 8) + 1;
$extraBits = ($byteLength * 8 - $numBits); $extraBits = ($byteLength * 8 - $numBits);
@ -258,21 +241,20 @@ final class BigInteger extends BigNumber
* *
* Using the default random bytes generator, this method is suitable for cryptographic use. * Using the default random bytes generator, this method is suitable for cryptographic use.
* *
* @psalm-param (callable(int): string)|null $randomBytesGenerator * @param BigNumber|int|float|string $min The lower bound. Must be convertible to a BigInteger.
* * @param BigNumber|int|float|string $max The upper bound. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $min The lower bound. Must be convertible to a BigInteger. * @param (callable(int): string)|null $randomBytesGenerator A function that accepts a number of bytes, and returns
* @param BigNumber|int|float|string $max The upper bound. Must be convertible to a BigInteger. * a string of random bytes of the given length. Defaults
* @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, * to the `random_bytes()` function.
* and returns a string of random bytes of the given length.
* Defaults to the `random_bytes()` function.
*
* @return BigInteger
* *
* @throws MathException If one of the parameters cannot be converted to a BigInteger, * @throws MathException If one of the parameters cannot be converted to a BigInteger,
* or `$min` is greater than `$max`. * or `$min` is greater than `$max`.
*/ */
public static function randomRange($min, $max, ?callable $randomBytesGenerator = null) : BigInteger public static function randomRange(
{ BigNumber|int|float|string $min,
BigNumber|int|float|string $max,
?callable $randomBytesGenerator = null
) : BigInteger {
$min = BigInteger::of($min); $min = BigInteger::of($min);
$max = BigInteger::of($max); $max = BigInteger::of($max);
@ -298,16 +280,11 @@ final class BigInteger extends BigNumber
/** /**
* Returns a BigInteger representing zero. * Returns a BigInteger representing zero.
* *
* @return BigInteger * @pure
*
* @psalm-pure
*/ */
public static function zero() : BigInteger public static function zero() : BigInteger
{ {
/** /** @var BigInteger|null $zero */
* @psalm-suppress ImpureStaticVariable
* @var BigInteger|null $zero
*/
static $zero; static $zero;
if ($zero === null) { if ($zero === null) {
@ -320,16 +297,11 @@ final class BigInteger extends BigNumber
/** /**
* Returns a BigInteger representing one. * Returns a BigInteger representing one.
* *
* @return BigInteger * @pure
*
* @psalm-pure
*/ */
public static function one() : BigInteger public static function one() : BigInteger
{ {
/** /** @var BigInteger|null $one */
* @psalm-suppress ImpureStaticVariable
* @var BigInteger|null $one
*/
static $one; static $one;
if ($one === null) { if ($one === null) {
@ -342,16 +314,11 @@ final class BigInteger extends BigNumber
/** /**
* Returns a BigInteger representing ten. * Returns a BigInteger representing ten.
* *
* @return BigInteger * @pure
*
* @psalm-pure
*/ */
public static function ten() : BigInteger public static function ten() : BigInteger
{ {
/** /** @var BigInteger|null $ten */
* @psalm-suppress ImpureStaticVariable
* @var BigInteger|null $ten
*/
static $ten; static $ten;
if ($ten === null) { if ($ten === null) {
@ -361,16 +328,34 @@ final class BigInteger extends BigNumber
return $ten; return $ten;
} }
/**
* @pure
*/
public static function gcdMultiple(BigInteger $a, BigInteger ...$n): BigInteger
{
$result = $a;
foreach ($n as $next) {
$result = $result->gcd($next);
if ($result->isEqualTo(1)) {
return $result;
}
}
return $result;
}
/** /**
* Returns the sum of this number and the given one. * Returns the sum of this number and the given one.
* *
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigInteger.
* *
* @return BigInteger The result.
*
* @throws MathException If the number is not valid, or is not convertible to a BigInteger. * @throws MathException If the number is not valid, or is not convertible to a BigInteger.
*
* @pure
*/ */
public function plus($that) : BigInteger public function plus(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -382,7 +367,7 @@ final class BigInteger extends BigNumber
return $that; return $that;
} }
$value = Calculator::get()->add($this->value, $that->value); $value = CalculatorRegistry::get()->add($this->value, $that->value);
return new BigInteger($value); return new BigInteger($value);
} }
@ -392,11 +377,11 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigInteger.
* *
* @return BigInteger The result.
*
* @throws MathException If the number is not valid, or is not convertible to a BigInteger. * @throws MathException If the number is not valid, or is not convertible to a BigInteger.
*
* @pure
*/ */
public function minus($that) : BigInteger public function minus(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -404,7 +389,7 @@ final class BigInteger extends BigNumber
return $this; return $this;
} }
$value = Calculator::get()->sub($this->value, $that->value); $value = CalculatorRegistry::get()->sub($this->value, $that->value);
return new BigInteger($value); return new BigInteger($value);
} }
@ -414,11 +399,11 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigInteger.
* *
* @return BigInteger The result.
*
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigInteger. * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigInteger.
*
* @pure
*/ */
public function multipliedBy($that) : BigInteger public function multipliedBy(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -430,7 +415,7 @@ final class BigInteger extends BigNumber
return $that; return $that;
} }
$value = Calculator::get()->mul($this->value, $that->value); $value = CalculatorRegistry::get()->mul($this->value, $that->value);
return new BigInteger($value); return new BigInteger($value);
} }
@ -439,14 +424,14 @@ final class BigInteger extends BigNumber
* Returns the result of the division of this number by the given one. * Returns the result of the division of this number by the given one.
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
* @param int $roundingMode An optional rounding mode. * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @return BigInteger The result.
* *
* @throws MathException If the divisor is not a valid number, is not convertible to a BigInteger, is zero, * @throws MathException If the divisor is not a valid number, is not convertible to a BigInteger, is zero,
* or RoundingMode::UNNECESSARY is used and the remainder is not zero. * or RoundingMode::UNNECESSARY is used and the remainder is not zero.
*
* @pure
*/ */
public function dividedBy($that, int $roundingMode = RoundingMode::UNNECESSARY) : BigInteger public function dividedBy(BigNumber|int|float|string $that, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -458,19 +443,40 @@ final class BigInteger extends BigNumber
throw DivisionByZeroException::divisionByZero(); throw DivisionByZeroException::divisionByZero();
} }
$result = Calculator::get()->divRound($this->value, $that->value, $roundingMode); $result = CalculatorRegistry::get()->divRound($this->value, $that->value, $roundingMode);
return new BigInteger($result); return new BigInteger($result);
} }
/**
* Limits (clamps) this number between the given minimum and maximum values.
*
* If the number is lower than $min, returns a copy of $min.
* If the number is greater than $max, returns a copy of $max.
* Otherwise, returns this number unchanged.
*
* @param BigNumber|int|float|string $min The minimum. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $max The maximum. Must be convertible to a BigInteger.
*
* @throws MathException If min/max are not convertible to a BigInteger.
*/
public function clamp(BigNumber|int|float|string $min, BigNumber|int|float|string $max) : BigInteger
{
if ($this->isLessThan($min)) {
return BigInteger::of($min);
} elseif ($this->isGreaterThan($max)) {
return BigInteger::of($max);
}
return $this;
}
/** /**
* Returns this number exponentiated to the given value. * Returns this number exponentiated to the given value.
* *
* @param int $exponent The exponent.
*
* @return BigInteger The result.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/ */
public function power(int $exponent) : BigInteger public function power(int $exponent) : BigInteger
{ {
@ -490,7 +496,7 @@ final class BigInteger extends BigNumber
)); ));
} }
return new BigInteger(Calculator::get()->pow($this->value, $exponent)); return new BigInteger(CalculatorRegistry::get()->pow($this->value, $exponent));
} }
/** /**
@ -498,11 +504,11 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
* *
* @return BigInteger
*
* @throws DivisionByZeroException If the divisor is zero. * @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function quotient($that) : BigInteger public function quotient(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -514,7 +520,7 @@ final class BigInteger extends BigNumber
throw DivisionByZeroException::divisionByZero(); throw DivisionByZeroException::divisionByZero();
} }
$quotient = Calculator::get()->divQ($this->value, $that->value); $quotient = CalculatorRegistry::get()->divQ($this->value, $that->value);
return new BigInteger($quotient); return new BigInteger($quotient);
} }
@ -526,11 +532,11 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
* *
* @return BigInteger
*
* @throws DivisionByZeroException If the divisor is zero. * @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function remainder($that) : BigInteger public function remainder(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -542,7 +548,7 @@ final class BigInteger extends BigNumber
throw DivisionByZeroException::divisionByZero(); throw DivisionByZeroException::divisionByZero();
} }
$remainder = Calculator::get()->divR($this->value, $that->value); $remainder = CalculatorRegistry::get()->divR($this->value, $that->value);
return new BigInteger($remainder); return new BigInteger($remainder);
} }
@ -552,11 +558,13 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
* *
* @return BigInteger[] An array containing the quotient and the remainder. * @return array{BigInteger, BigInteger} An array containing the quotient and the remainder.
* *
* @throws DivisionByZeroException If the divisor is zero. * @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function quotientAndRemainder($that) : array public function quotientAndRemainder(BigNumber|int|float|string $that) : array
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -564,7 +572,7 @@ final class BigInteger extends BigNumber
throw DivisionByZeroException::divisionByZero(); throw DivisionByZeroException::divisionByZero();
} }
[$quotient, $remainder] = Calculator::get()->divQR($this->value, $that->value); [$quotient, $remainder] = CalculatorRegistry::get()->divQR($this->value, $that->value);
return [ return [
new BigInteger($quotient), new BigInteger($quotient),
@ -582,11 +590,11 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
* *
* @return BigInteger
*
* @throws DivisionByZeroException If the divisor is zero. * @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function mod($that) : BigInteger public function mod(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -594,7 +602,7 @@ final class BigInteger extends BigNumber
throw DivisionByZeroException::modulusMustNotBeZero(); throw DivisionByZeroException::modulusMustNotBeZero();
} }
$value = Calculator::get()->mod($this->value, $that->value); $value = CalculatorRegistry::get()->mod($this->value, $that->value);
return new BigInteger($value); return new BigInteger($value);
} }
@ -602,14 +610,12 @@ final class BigInteger extends BigNumber
/** /**
* Returns the modular multiplicative inverse of this BigInteger modulo $m. * Returns the modular multiplicative inverse of this BigInteger modulo $m.
* *
* @param BigInteger $m
*
* @return BigInteger
*
* @throws DivisionByZeroException If $m is zero. * @throws DivisionByZeroException If $m is zero.
* @throws NegativeNumberException If $m is negative. * @throws NegativeNumberException If $m is negative.
* @throws MathException If this BigInteger has no multiplicative inverse mod m (that is, this BigInteger * @throws MathException If this BigInteger has no multiplicative inverse mod m (that is, this BigInteger
* is not relatively prime to m). * is not relatively prime to m).
*
* @pure
*/ */
public function modInverse(BigInteger $m) : BigInteger public function modInverse(BigInteger $m) : BigInteger
{ {
@ -625,7 +631,7 @@ final class BigInteger extends BigNumber
return BigInteger::zero(); return BigInteger::zero();
} }
$value = Calculator::get()->modInverse($this->value, $m->value); $value = CalculatorRegistry::get()->modInverse($this->value, $m->value);
if ($value === null) { if ($value === null) {
throw new MathException('Unable to compute the modInverse for the given modulus.'); throw new MathException('Unable to compute the modInverse for the given modulus.');
@ -642,12 +648,12 @@ final class BigInteger extends BigNumber
* @param BigNumber|int|float|string $exp The exponent. Must be positive or zero. * @param BigNumber|int|float|string $exp The exponent. Must be positive or zero.
* @param BigNumber|int|float|string $mod The modulus. Must be strictly positive. * @param BigNumber|int|float|string $mod The modulus. Must be strictly positive.
* *
* @return BigInteger
*
* @throws NegativeNumberException If any of the operands is negative. * @throws NegativeNumberException If any of the operands is negative.
* @throws DivisionByZeroException If the modulus is zero. * @throws DivisionByZeroException If the modulus is zero.
*
* @pure
*/ */
public function modPow($exp, $mod) : BigInteger public function modPow(BigNumber|int|float|string $exp, BigNumber|int|float|string $mod) : BigInteger
{ {
$exp = BigInteger::of($exp); $exp = BigInteger::of($exp);
$mod = BigInteger::of($mod); $mod = BigInteger::of($mod);
@ -660,7 +666,7 @@ final class BigInteger extends BigNumber
throw DivisionByZeroException::modulusMustNotBeZero(); throw DivisionByZeroException::modulusMustNotBeZero();
} }
$result = Calculator::get()->modPow($this->value, $exp->value, $mod->value); $result = CalculatorRegistry::get()->modPow($this->value, $exp->value, $mod->value);
return new BigInteger($result); return new BigInteger($result);
} }
@ -672,9 +678,9 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
* *
* @return BigInteger * @pure
*/ */
public function gcd($that) : BigInteger public function gcd(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
@ -686,7 +692,7 @@ final class BigInteger extends BigNumber
return $that; return $that;
} }
$value = Calculator::get()->gcd($this->value, $that->value); $value = CalculatorRegistry::get()->gcd($this->value, $that->value);
return new BigInteger($value); return new BigInteger($value);
} }
@ -696,9 +702,9 @@ final class BigInteger extends BigNumber
* *
* The result is the largest x such that n. * The result is the largest x such that n.
* *
* @return BigInteger
*
* @throws NegativeNumberException If this number is negative. * @throws NegativeNumberException If this number is negative.
*
* @pure
*/ */
public function sqrt() : BigInteger public function sqrt() : BigInteger
{ {
@ -706,7 +712,7 @@ final class BigInteger extends BigNumber
throw new NegativeNumberException('Cannot calculate the square root of a negative number.'); throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
} }
$value = Calculator::get()->sqrt($this->value); $value = CalculatorRegistry::get()->sqrt($this->value);
return new BigInteger($value); return new BigInteger($value);
} }
@ -714,7 +720,7 @@ final class BigInteger extends BigNumber
/** /**
* Returns the absolute value of this number. * Returns the absolute value of this number.
* *
* @return BigInteger * @pure
*/ */
public function abs() : BigInteger public function abs() : BigInteger
{ {
@ -724,11 +730,11 @@ final class BigInteger extends BigNumber
/** /**
* Returns the inverse of this number. * Returns the inverse of this number.
* *
* @return BigInteger * @pure
*/ */
public function negated() : BigInteger public function negated() : BigInteger
{ {
return new BigInteger(Calculator::get()->neg($this->value)); return new BigInteger(CalculatorRegistry::get()->neg($this->value));
} }
/** /**
@ -738,13 +744,13 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
* *
* @return BigInteger * @pure
*/ */
public function and($that) : BigInteger public function and(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
return new BigInteger(Calculator::get()->and($this->value, $that->value)); return new BigInteger(CalculatorRegistry::get()->and($this->value, $that->value));
} }
/** /**
@ -754,13 +760,13 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
* *
* @return BigInteger * @pure
*/ */
public function or($that) : BigInteger public function or(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
return new BigInteger(Calculator::get()->or($this->value, $that->value)); return new BigInteger(CalculatorRegistry::get()->or($this->value, $that->value));
} }
/** /**
@ -770,19 +776,19 @@ final class BigInteger extends BigNumber
* *
* @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
* *
* @return BigInteger * @pure
*/ */
public function xor($that) : BigInteger public function xor(BigNumber|int|float|string $that) : BigInteger
{ {
$that = BigInteger::of($that); $that = BigInteger::of($that);
return new BigInteger(Calculator::get()->xor($this->value, $that->value)); return new BigInteger(CalculatorRegistry::get()->xor($this->value, $that->value));
} }
/** /**
* Returns the bitwise-not of this BigInteger. * Returns the bitwise-not of this BigInteger.
* *
* @return BigInteger * @pure
*/ */
public function not() : BigInteger public function not() : BigInteger
{ {
@ -792,9 +798,7 @@ final class BigInteger extends BigNumber
/** /**
* Returns the integer left shifted by a given number of bits. * Returns the integer left shifted by a given number of bits.
* *
* @param int $distance The distance to shift. * @pure
*
* @return BigInteger
*/ */
public function shiftedLeft(int $distance) : BigInteger public function shiftedLeft(int $distance) : BigInteger
{ {
@ -812,9 +816,7 @@ final class BigInteger extends BigNumber
/** /**
* Returns the integer right shifted by a given number of bits. * Returns the integer right shifted by a given number of bits.
* *
* @param int $distance The distance to shift. * @pure
*
* @return BigInteger
*/ */
public function shiftedRight(int $distance) : BigInteger public function shiftedRight(int $distance) : BigInteger
{ {
@ -841,7 +843,7 @@ final class BigInteger extends BigNumber
* For positive BigIntegers, this is equivalent to the number of bits in the ordinary binary representation. * For positive BigIntegers, this is equivalent to the number of bits in the ordinary binary representation.
* Computes (ceil(log2(this < 0 ? -this : this+1))). * Computes (ceil(log2(this < 0 ? -this : this+1))).
* *
* @return int * @pure
*/ */
public function getBitLength() : int public function getBitLength() : int
{ {
@ -861,7 +863,7 @@ final class BigInteger extends BigNumber
* *
* Returns -1 if this BigInteger contains no one bits. * Returns -1 if this BigInteger contains no one bits.
* *
* @return int * @pure
*/ */
public function getLowestSetBit() : int public function getLowestSetBit() : int
{ {
@ -882,7 +884,7 @@ final class BigInteger extends BigNumber
/** /**
* Returns whether this number is even. * Returns whether this number is even.
* *
* @return bool * @pure
*/ */
public function isEven() : bool public function isEven() : bool
{ {
@ -892,7 +894,7 @@ final class BigInteger extends BigNumber
/** /**
* Returns whether this number is odd. * Returns whether this number is odd.
* *
* @return bool * @pure
*/ */
public function isOdd() : bool public function isOdd() : bool
{ {
@ -906,9 +908,9 @@ final class BigInteger extends BigNumber
* *
* @param int $n The bit to test, 0-based. * @param int $n The bit to test, 0-based.
* *
* @return bool
*
* @throws \InvalidArgumentException If the bit to test is negative. * @throws \InvalidArgumentException If the bit to test is negative.
*
* @pure
*/ */
public function testBit(int $n) : bool public function testBit(int $n) : bool
{ {
@ -919,63 +921,49 @@ final class BigInteger extends BigNumber
return $this->shiftedRight($n)->isOdd(); return $this->shiftedRight($n)->isOdd();
} }
/** #[Override]
* {@inheritdoc} public function compareTo(BigNumber|int|float|string $that) : int
*/
public function compareTo($that) : int
{ {
$that = BigNumber::of($that); $that = BigNumber::of($that);
if ($that instanceof BigInteger) { if ($that instanceof BigInteger) {
return Calculator::get()->cmp($this->value, $that->value); return CalculatorRegistry::get()->cmp($this->value, $that->value);
} }
return - $that->compareTo($this); return - $that->compareTo($this);
} }
/** #[Override]
* {@inheritdoc}
*/
public function getSign() : int public function getSign() : int
{ {
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigInteger() : BigInteger public function toBigInteger() : BigInteger
{ {
return $this; return $this;
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigDecimal() : BigDecimal public function toBigDecimal() : BigDecimal
{ {
return BigDecimal::create($this->value); return self::newBigDecimal($this->value);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigRational() : BigRational public function toBigRational() : BigRational
{ {
return BigRational::create($this, BigInteger::one(), false); return self::newBigRational($this, BigInteger::one(), false);
} }
/** #[Override]
* {@inheritdoc} public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
*/
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{ {
return $this->toBigDecimal()->toScale($scale, $roundingMode); return $this->toBigDecimal()->toScale($scale, $roundingMode);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toInt() : int public function toInt() : int
{ {
$intValue = (int) $this->value; $intValue = (int) $this->value;
@ -987,9 +975,7 @@ final class BigInteger extends BigNumber
return $intValue; return $intValue;
} }
/** #[Override]
* {@inheritdoc}
*/
public function toFloat() : float public function toFloat() : float
{ {
return (float) $this->value; return (float) $this->value;
@ -1000,11 +986,9 @@ final class BigInteger extends BigNumber
* *
* The output will always be lowercase for bases greater than 10. * The output will always be lowercase for bases greater than 10.
* *
* @param int $base
*
* @return string
*
* @throws \InvalidArgumentException If the base is out of range. * @throws \InvalidArgumentException If the base is out of range.
*
* @pure
*/ */
public function toBase(int $base) : string public function toBase(int $base) : string
{ {
@ -1016,7 +1000,7 @@ final class BigInteger extends BigNumber
throw new \InvalidArgumentException(\sprintf('Base %d is out of range [2, 36]', $base)); throw new \InvalidArgumentException(\sprintf('Base %d is out of range [2, 36]', $base));
} }
return Calculator::get()->toBase($this->value, $base); return CalculatorRegistry::get()->toBase($this->value, $base);
} }
/** /**
@ -1027,10 +1011,10 @@ final class BigInteger extends BigNumber
* *
* @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8. * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8.
* *
* @return string
*
* @throws NegativeNumberException If this number is negative. * @throws NegativeNumberException If this number is negative.
* @throws \InvalidArgumentException If the given alphabet does not contain at least 2 chars. * @throws \InvalidArgumentException If the given alphabet does not contain at least 2 chars.
*
* @pure
*/ */
public function toArbitraryBase(string $alphabet) : string public function toArbitraryBase(string $alphabet) : string
{ {
@ -1044,7 +1028,7 @@ final class BigInteger extends BigNumber
throw new NegativeNumberException(__FUNCTION__ . '() does not support negative numbers.'); throw new NegativeNumberException(__FUNCTION__ . '() does not support negative numbers.');
} }
return Calculator::get()->toArbitraryBase($this->value, $alphabet, $base); return CalculatorRegistry::get()->toArbitraryBase($this->value, $alphabet, $base);
} }
/** /**
@ -1063,9 +1047,9 @@ final class BigInteger extends BigNumber
* *
* @param bool $signed Whether to output a signed number in two's-complement representation with a leading sign bit. * @param bool $signed Whether to output a signed number in two's-complement representation with a leading sign bit.
* *
* @return string
*
* @throws NegativeNumberException If $signed is false, and the number is negative. * @throws NegativeNumberException If $signed is false, and the number is negative.
*
* @pure
*/ */
public function toBytes(bool $signed = true) : string public function toBytes(bool $signed = true) : string
{ {
@ -1105,14 +1089,19 @@ final class BigInteger extends BigNumber
} }
} }
return \hex2bin($hex); $result = \hex2bin($hex);
assert($result !== false);
return $result;
} }
/** /**
* {@inheritdoc} * @return numeric-string
*/ */
#[Override]
public function __toString() : string public function __toString() : string
{ {
/** @var numeric-string */
return $this->value; return $this->value;
} }
@ -1132,53 +1121,19 @@ final class BigInteger extends BigNumber
* This method is only here to allow unserializing the object and cannot be accessed directly. * This method is only here to allow unserializing the object and cannot be accessed directly.
* *
* @internal * @internal
* @psalm-suppress RedundantPropertyInitializationCheck
* *
* @param array{value: string} $data * @param array{value: string} $data
* *
* @return void
*
* @throws \LogicException * @throws \LogicException
*/ */
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->value)) { if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
} }
/** @phpstan-ignore deadCode.unreachable */
$this->value = $data['value']; $this->value = $data['value'];
} }
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*
* @return string
*/
public function serialize() : string
{
return $this->value;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param string $value
*
* @return void
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->value)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
$this->value = $value;
}
} }

View File

@ -8,38 +8,46 @@ use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException; use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException; use Brick\Math\Exception\RoundingNecessaryException;
use Override;
/** /**
* Common interface for arbitrary-precision rational numbers. * Base class for arbitrary-precision numbers.
* *
* @psalm-immutable * This class is sealed: it is part of the public API but should not be subclassed in userland.
* Protected methods may change in any version.
*
* @phpstan-sealed BigInteger|BigDecimal|BigRational
*/ */
abstract class BigNumber implements \Serializable, \JsonSerializable abstract readonly class BigNumber implements \JsonSerializable, \Stringable
{ {
/** /**
* The regular expression used to parse integer, decimal and rational numbers. * The regular expression used to parse integer or decimal numbers.
*/ */
private const PARSE_REGEXP = private const PARSE_REGEXP_NUMERICAL =
'/^' . '/^' .
'(?<sign>[\-\+])?' . '(?<sign>[\-\+])?' .
'(?:' . '(?<integral>[0-9]+)?' .
'(?:' . '(?<point>\.)?' .
'(?<integral>[0-9]+)?' . '(?<fractional>[0-9]+)?' .
'(?<point>\.)?' . '(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
'(?<fractional>[0-9]+)?' . '$/';
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
')|(?:' . /**
'(?<numerator>[0-9]+)' . * The regular expression used to parse rational numbers.
'\/?' . */
'(?<denominator>[0-9]+)' . private const PARSE_REGEXP_RATIONAL =
')' . '/^' .
')' . '(?<sign>[\-\+])?' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
'$/'; '$/';
/** /**
* Creates a BigNumber of the given value. * Creates a BigNumber of the given value.
* *
* The concrete return type is dependent on the given value, with the following rules: * When of() is called on BigNumber, the concrete return type is dependent on the given value, with the following
* rules:
* *
* - BigNumber instances are returned as is * - BigNumber instances are returned as is
* - integer numbers are returned as BigInteger * - integer numbers are returned as BigInteger
@ -48,16 +56,35 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal * - strings containing a `.` character or using an exponential notation are returned as BigDecimal
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
* *
* @param BigNumber|int|float|string $value * When of() is called on BigInteger, BigDecimal, or BigRational, the resulting number is converted to an instance
* of the subclass when possible; otherwise a RoundingNecessaryException exception is thrown.
* *
* @return BigNumber * @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
* *
* @throws NumberFormatException If the format of the number is not valid. * @pure
*/
final public static function of(BigNumber|int|float|string $value) : static
{
$value = self::_of($value);
if (static::class === BigNumber::class) {
assert($value instanceof static);
return $value;
}
return static::from($value);
}
/**
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
* *
* @psalm-pure * @pure
*/ */
public static function of($value) : BigNumber private static function _of(BigNumber|int|float|string $value) : BigNumber
{ {
if ($value instanceof BigNumber) { if ($value instanceof BigNumber) {
return $value; return $value;
@ -67,37 +94,22 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
return new BigInteger((string) $value); return new BigInteger((string) $value);
} }
/** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */ if (is_float($value)) {
$value = \is_float($value) ? self::floatToString($value) : (string) $value; $value = (string) $value;
$throw = static function() use ($value) : void {
throw new NumberFormatException(\sprintf(
'The given value "%s" does not represent a valid number.',
$value
));
};
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
$throw();
} }
$getMatch = static function(string $value) use ($matches) : ?string { if (str_contains($value, '/')) {
return isset($matches[$value]) && $matches[$value] !== '' ? $matches[$value] : null; // Rational number
}; if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
$sign = $getMatch('sign');
$numerator = $getMatch('numerator');
$denominator = $getMatch('denominator');
if ($numerator !== null) {
assert($denominator !== null);
if ($sign !== null) {
$numerator = $sign . $numerator;
} }
$numerator = self::cleanUp($numerator); $sign = $matches['sign'];
$denominator = self::cleanUp($denominator); $numerator = $matches['numerator'];
$denominator = $matches['denominator'];
$numerator = self::cleanUp($sign, $numerator);
$denominator = self::cleanUp(null, $denominator);
if ($denominator === '0') { if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero(); throw DivisionByZeroException::denominatorMustNotBeZero();
@ -108,88 +120,94 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
new BigInteger($denominator), new BigInteger($denominator),
false false
); );
} } else {
// Integer or decimal number
$point = $getMatch('point'); if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
$integral = $getMatch('integral'); throw NumberFormatException::invalidFormat($value);
$fractional = $getMatch('fractional');
$exponent = $getMatch('exponent');
if ($integral === null && $fractional === null) {
$throw();
}
if ($integral === null) {
$integral = '0';
}
if ($point !== null || $exponent !== null) {
$fractional = ($fractional ?? '');
$exponent = ($exponent !== null) ? (int) $exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
throw new NumberFormatException('Exponent too large.');
} }
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional); $sign = $matches['sign'];
$point = $matches['point'];
$integral = $matches['integral'];
$fractional = $matches['fractional'];
$exponent = $matches['exponent'];
$scale = \strlen($fractional) - $exponent; if ($integral === null && $fractional === null) {
throw NumberFormatException::invalidFormat($value);
}
if ($scale < 0) { if ($integral === null) {
if ($unscaledValue !== '0') { $integral = '0';
$unscaledValue .= \str_repeat('0', - $scale); }
if ($point !== null || $exponent !== null) {
$fractional ??= '';
$exponent = ($exponent !== null) ? (int)$exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
throw new NumberFormatException('Exponent too large.');
} }
$scale = 0;
$unscaledValue = self::cleanUp($sign, $integral . $fractional);
$scale = \strlen($fractional) - $exponent;
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($unscaledValue, $scale);
} }
return new BigDecimal($unscaledValue, $scale); $integral = self::cleanUp($sign, $integral);
return new BigInteger($integral);
} }
$integral = self::cleanUp(($sign ?? '') . $integral);
return new BigInteger($integral);
} }
/** /**
* Safely converts float to string, avoiding locale-dependent issues. * Overridden by subclasses to convert a BigNumber to an instance of the subclass.
* *
* @see https://github.com/brick/math/pull/20 * @throws RoundingNecessaryException If the value cannot be converted.
* *
* @param float $float * @pure
*
* @return string
*
* @psalm-pure
* @psalm-suppress ImpureFunctionCall
*/ */
private static function floatToString(float $float) : string abstract protected static function from(BigNumber $number): static;
{
$currentLocale = \setlocale(LC_NUMERIC, '0');
\setlocale(LC_NUMERIC, 'C');
$result = (string) $float;
\setlocale(LC_NUMERIC, $currentLocale);
return $result;
}
/** /**
* Proxy method to access protected constructors from sibling classes. * Proxy method to access BigInteger's protected constructor from sibling classes.
* *
* @pure
* @internal * @internal
*
* @param mixed ...$args The arguments to the constructor.
*
* @return static
*
* @psalm-pure
* @psalm-suppress TooManyArguments
* @psalm-suppress UnsafeInstantiation
*/ */
protected static function create(... $args) : BigNumber final protected function newBigInteger(string $value) : BigInteger
{ {
return new static(... $args); return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @pure
* @internal
*/
final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @pure
* @internal
*/
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
} }
/** /**
@ -198,16 +216,12 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on. * to an instance of the class this method is called on.
* *
* @return static The minimum value.
*
* @throws \InvalidArgumentException If no values are given. * @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid. * @throws MathException If an argument is not valid.
* *
* @psalm-suppress LessSpecificReturnStatement * @pure
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/ */
public static function min(...$values) : BigNumber final public static function min(BigNumber|int|float|string ...$values) : static
{ {
$min = null; $min = null;
@ -232,16 +246,12 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on. * to an instance of the class this method is called on.
* *
* @return static The maximum value.
*
* @throws \InvalidArgumentException If no values are given. * @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid. * @throws MathException If an argument is not valid.
* *
* @psalm-suppress LessSpecificReturnStatement * @pure
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/ */
public static function max(...$values) : BigNumber final public static function max(BigNumber|int|float|string ...$values) : static
{ {
$max = null; $max = null;
@ -263,50 +273,43 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Returns the sum of the given values. * Returns the sum of the given values.
* *
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible * When called on BigNumber, sum() accepts any supported type and returns a result whose type is the widest among
* to an instance of the class this method is called on. * the given values (BigInteger < BigDecimal < BigRational).
* *
* @return static The sum. * When called on BigInteger, BigDecimal, or BigRational, sum() requires that all values can be converted to that
* specific subclass, and returns a result of the same type.
*
* @param BigNumber|int|float|string ...$values The values to add. All values must be convertible to the class on
* which this method is called.
* *
* @throws \InvalidArgumentException If no values are given. * @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid. * @throws MathException If an argument is not valid.
* *
* @psalm-suppress LessSpecificReturnStatement * @pure
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/ */
public static function sum(...$values) : BigNumber final public static function sum(BigNumber|int|float|string ...$values) : static
{ {
/** @var BigNumber|null $sum */ $first = array_shift($values);
$sum = null;
foreach ($values as $value) { if ($first === null) {
$value = static::of($value);
$sum = $sum === null ? $value : self::add($sum, $value);
}
if ($sum === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
} }
$sum = static::of($first);
foreach ($values as $value) {
$sum = self::add($sum, static::of($value));
}
assert($sum instanceof static);
return $sum; return $sum;
} }
/** /**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException. * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
* *
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to * @pure
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
* depending on their ability to perform the operation. This will also require a version bump because we're
* potentially breaking custom BigNumber implementations (if any...)
*
* @param BigNumber $a
* @param BigNumber $b
*
* @return BigNumber
*
* @psalm-pure
*/ */
private static function add(BigNumber $a, BigNumber $b) : BigNumber private static function add(BigNumber $a, BigNumber $b) : BigNumber
{ {
@ -326,49 +329,34 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
return $b->plus($a); return $b->plus($a);
} }
/** @var BigInteger $a */
return $a->plus($b); return $a->plus($b);
} }
/** /**
* Removes optional leading zeros and + sign from the given number. * Removes optional leading zeros and applies sign.
* *
* @param string $number The number, validated as a non-empty string of digits with optional leading sign. * @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
* @param string $number The number, validated as a string of digits.
* *
* @return string * @pure
*
* @psalm-pure
*/ */
private static function cleanUp(string $number) : string private static function cleanUp(string|null $sign, string $number) : string
{ {
$firstChar = $number[0];
if ($firstChar === '+' || $firstChar === '-') {
$number = \substr($number, 1);
}
$number = \ltrim($number, '0'); $number = \ltrim($number, '0');
if ($number === '') { if ($number === '') {
return '0'; return '0';
} }
if ($firstChar === '-') { return $sign === '-' ? '-' . $number : $number;
return '-' . $number;
}
return $number;
} }
/** /**
* Checks if this number is equal to the given one. * Checks if this number is equal to the given one.
* *
* @param BigNumber|int|float|string $that * @pure
*
* @return bool
*/ */
public function isEqualTo($that) : bool final public function isEqualTo(BigNumber|int|float|string $that) : bool
{ {
return $this->compareTo($that) === 0; return $this->compareTo($that) === 0;
} }
@ -376,11 +364,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is strictly lower than the given one. * Checks if this number is strictly lower than the given one.
* *
* @param BigNumber|int|float|string $that * @pure
*
* @return bool
*/ */
public function isLessThan($that) : bool final public function isLessThan(BigNumber|int|float|string $that) : bool
{ {
return $this->compareTo($that) < 0; return $this->compareTo($that) < 0;
} }
@ -388,11 +374,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is lower than or equal to the given one. * Checks if this number is lower than or equal to the given one.
* *
* @param BigNumber|int|float|string $that * @pure
*
* @return bool
*/ */
public function isLessThanOrEqualTo($that) : bool final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
{ {
return $this->compareTo($that) <= 0; return $this->compareTo($that) <= 0;
} }
@ -400,11 +384,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is strictly greater than the given one. * Checks if this number is strictly greater than the given one.
* *
* @param BigNumber|int|float|string $that * @pure
*
* @return bool
*/ */
public function isGreaterThan($that) : bool final public function isGreaterThan(BigNumber|int|float|string $that) : bool
{ {
return $this->compareTo($that) > 0; return $this->compareTo($that) > 0;
} }
@ -412,11 +394,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is greater than or equal to the given one. * Checks if this number is greater than or equal to the given one.
* *
* @param BigNumber|int|float|string $that * @pure
*
* @return bool
*/ */
public function isGreaterThanOrEqualTo($that) : bool final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{ {
return $this->compareTo($that) >= 0; return $this->compareTo($that) >= 0;
} }
@ -424,9 +404,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number equals zero. * Checks if this number equals zero.
* *
* @return bool * @pure
*/ */
public function isZero() : bool final public function isZero() : bool
{ {
return $this->getSign() === 0; return $this->getSign() === 0;
} }
@ -434,9 +414,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is strictly negative. * Checks if this number is strictly negative.
* *
* @return bool * @pure
*/ */
public function isNegative() : bool final public function isNegative() : bool
{ {
return $this->getSign() < 0; return $this->getSign() < 0;
} }
@ -444,9 +424,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is negative or zero. * Checks if this number is negative or zero.
* *
* @return bool * @pure
*/ */
public function isNegativeOrZero() : bool final public function isNegativeOrZero() : bool
{ {
return $this->getSign() <= 0; return $this->getSign() <= 0;
} }
@ -454,9 +434,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is strictly positive. * Checks if this number is strictly positive.
* *
* @return bool * @pure
*/ */
public function isPositive() : bool final public function isPositive() : bool
{ {
return $this->getSign() > 0; return $this->getSign() > 0;
} }
@ -464,9 +444,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Checks if this number is positive or zero. * Checks if this number is positive or zero.
* *
* @return bool * @pure
*/ */
public function isPositiveOrZero() : bool final public function isPositiveOrZero() : bool
{ {
return $this->getSign() >= 0; return $this->getSign() >= 0;
} }
@ -474,58 +454,64 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/** /**
* Returns the sign of this number. * Returns the sign of this number.
* *
* @return int -1 if the number is negative, 0 if zero, 1 if positive. * Returns -1 if the number is negative, 0 if zero, 1 if positive.
*
* @return -1|0|1
*
* @pure
*/ */
abstract public function getSign() : int; abstract public function getSign() : int;
/** /**
* Compares this number to the given one. * Compares this number to the given one.
* *
* @param BigNumber|int|float|string $that * Returns -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
* *
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`. * @return -1|0|1
* *
* @throws MathException If the number is not valid. * @throws MathException If the number is not valid.
*
* @pure
*/ */
abstract public function compareTo($that) : int; abstract public function compareTo(BigNumber|int|float|string $that) : int;
/** /**
* Converts this number to a BigInteger. * Converts this number to a BigInteger.
* *
* @return BigInteger The converted number.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding. * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*
* @pure
*/ */
abstract public function toBigInteger() : BigInteger; abstract public function toBigInteger() : BigInteger;
/** /**
* Converts this number to a BigDecimal. * Converts this number to a BigDecimal.
* *
* @return BigDecimal The converted number.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding. * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*
* @pure
*/ */
abstract public function toBigDecimal() : BigDecimal; abstract public function toBigDecimal() : BigDecimal;
/** /**
* Converts this number to a BigRational. * Converts this number to a BigRational.
* *
* @return BigRational The converted number. * @pure
*/ */
abstract public function toBigRational() : BigRational; abstract public function toBigRational() : BigRational;
/** /**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary. * Converts this number to a BigDecimal with the given scale, using rounding if necessary.
* *
* @param int $scale The scale of the resulting `BigDecimal`. * @param int $scale The scale of the resulting `BigDecimal`.
* @param int $roundingMode A `RoundingMode` constant. * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @return BigDecimal
* *
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding. * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used. * This only applies when RoundingMode::UNNECESSARY is used.
*
* @pure
*/ */
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal; abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/** /**
* Returns the exact value of this number as a native integer. * Returns the exact value of this number as a native integer.
@ -533,9 +519,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* If this number cannot be converted to a native integer without losing precision, an exception is thrown. * If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit. * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
* *
* @return int The converted value.
*
* @throws MathException If this number cannot be exactly converted to a native integer. * @throws MathException If this number cannot be exactly converted to a native integer.
*
* @pure
*/ */
abstract public function toInt() : int; abstract public function toInt() : int;
@ -548,7 +534,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* If the number is greater than the largest representable floating point number, positive infinity is returned. * If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned. * If the number is less than the smallest representable floating point number, negative infinity is returned.
* *
* @return float The converted value. * @pure
*/ */
abstract public function toFloat() : float; abstract public function toFloat() : float;
@ -558,14 +544,12 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* The output of this method can be parsed by the `of()` factory method; * The output of this method can be parsed by the `of()` factory method;
* this will yield an object equal to this one, without any information loss. * this will yield an object equal to this one, without any information loss.
* *
* @return string * @pure
*/ */
abstract public function __toString() : string; abstract public function __toString() : string;
/** #[Override]
* {@inheritdoc} final public function jsonSerialize() : string
*/
public function jsonSerialize() : string
{ {
return $this->__toString(); return $this->__toString();
} }

View File

@ -8,29 +8,24 @@ use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException; use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException; use Brick\Math\Exception\RoundingNecessaryException;
use Override;
/** /**
* An arbitrarily large rational number. * An arbitrarily large rational number.
* *
* This class is immutable. * This class is immutable.
*
* @psalm-immutable
*/ */
final class BigRational extends BigNumber final readonly class BigRational extends BigNumber
{ {
/** /**
* The numerator. * The numerator.
*
* @var BigInteger
*/ */
private $numerator; private BigInteger $numerator;
/** /**
* The denominator. Always strictly positive. * The denominator. Always strictly positive.
*
* @var BigInteger
*/ */
private $denominator; private BigInteger $denominator;
/** /**
* Protected constructor. Use a factory method to obtain an instance. * Protected constructor. Use a factory method to obtain an instance.
@ -40,6 +35,8 @@ final class BigRational extends BigNumber
* @param bool $checkDenominator Whether to check the denominator for negative and zero. * @param bool $checkDenominator Whether to check the denominator for negative and zero.
* *
* @throws DivisionByZeroException If the denominator is zero. * @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/ */
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{ {
@ -58,20 +55,10 @@ final class BigRational extends BigNumber
$this->denominator = $denominator; $this->denominator = $denominator;
} }
/** #[Override]
* Creates a BigRational of the given value. protected static function from(BigNumber $number): static
*
* @param BigNumber|int|float|string $value
*
* @return BigRational
*
* @throws MathException If the value cannot be converted to a BigRational.
*
* @psalm-pure
*/
public static function of($value) : BigNumber
{ {
return parent::of($value)->toBigRational(); return $number->toBigRational();
} }
/** /**
@ -83,16 +70,16 @@ final class BigRational extends BigNumber
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
* *
* @return BigRational
*
* @throws NumberFormatException If an argument does not represent a valid number. * @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number. * @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero. * @throws DivisionByZeroException If the denominator is zero.
* *
* @psalm-pure * @pure
*/ */
public static function nd($numerator, $denominator) : BigRational public static function nd(
{ BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
) : BigRational {
$numerator = BigInteger::of($numerator); $numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator); $denominator = BigInteger::of($denominator);
@ -102,16 +89,11 @@ final class BigRational extends BigNumber
/** /**
* Returns a BigRational representing zero. * Returns a BigRational representing zero.
* *
* @return BigRational * @pure
*
* @psalm-pure
*/ */
public static function zero() : BigRational public static function zero() : BigRational
{ {
/** /** @var BigRational|null $zero */
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $zero
*/
static $zero; static $zero;
if ($zero === null) { if ($zero === null) {
@ -124,16 +106,11 @@ final class BigRational extends BigNumber
/** /**
* Returns a BigRational representing one. * Returns a BigRational representing one.
* *
* @return BigRational * @pure
*
* @psalm-pure
*/ */
public static function one() : BigRational public static function one() : BigRational
{ {
/** /** @var BigRational|null $one */
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $one
*/
static $one; static $one;
if ($one === null) { if ($one === null) {
@ -146,16 +123,11 @@ final class BigRational extends BigNumber
/** /**
* Returns a BigRational representing ten. * Returns a BigRational representing ten.
* *
* @return BigRational * @pure
*
* @psalm-pure
*/ */
public static function ten() : BigRational public static function ten() : BigRational
{ {
/** /** @var BigRational|null $ten */
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $ten
*/
static $ten; static $ten;
if ($ten === null) { if ($ten === null) {
@ -166,7 +138,7 @@ final class BigRational extends BigNumber
} }
/** /**
* @return BigInteger * @pure
*/ */
public function getNumerator() : BigInteger public function getNumerator() : BigInteger
{ {
@ -174,7 +146,7 @@ final class BigRational extends BigNumber
} }
/** /**
* @return BigInteger * @pure
*/ */
public function getDenominator() : BigInteger public function getDenominator() : BigInteger
{ {
@ -184,7 +156,7 @@ final class BigRational extends BigNumber
/** /**
* Returns the quotient of the division of the numerator by the denominator. * Returns the quotient of the division of the numerator by the denominator.
* *
* @return BigInteger * @pure
*/ */
public function quotient() : BigInteger public function quotient() : BigInteger
{ {
@ -194,7 +166,7 @@ final class BigRational extends BigNumber
/** /**
* Returns the remainder of the division of the numerator by the denominator. * Returns the remainder of the division of the numerator by the denominator.
* *
* @return BigInteger * @pure
*/ */
public function remainder() : BigInteger public function remainder() : BigInteger
{ {
@ -204,7 +176,9 @@ final class BigRational extends BigNumber
/** /**
* Returns the quotient and remainder of the division of the numerator by the denominator. * Returns the quotient and remainder of the division of the numerator by the denominator.
* *
* @return BigInteger[] * @return array{BigInteger, BigInteger}
*
* @pure
*/ */
public function quotientAndRemainder() : array public function quotientAndRemainder() : array
{ {
@ -216,11 +190,11 @@ final class BigRational extends BigNumber
* *
* @param BigNumber|int|float|string $that The number to add. * @param BigNumber|int|float|string $that The number to add.
* *
* @return BigRational The result.
*
* @throws MathException If the number is not valid. * @throws MathException If the number is not valid.
*
* @pure
*/ */
public function plus($that) : BigRational public function plus(BigNumber|int|float|string $that) : BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@ -236,11 +210,11 @@ final class BigRational extends BigNumber
* *
* @param BigNumber|int|float|string $that The number to subtract. * @param BigNumber|int|float|string $that The number to subtract.
* *
* @return BigRational The result.
*
* @throws MathException If the number is not valid. * @throws MathException If the number is not valid.
*
* @pure
*/ */
public function minus($that) : BigRational public function minus(BigNumber|int|float|string $that) : BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@ -256,11 +230,11 @@ final class BigRational extends BigNumber
* *
* @param BigNumber|int|float|string $that The multiplier. * @param BigNumber|int|float|string $that The multiplier.
* *
* @return BigRational The result.
*
* @throws MathException If the multiplier is not a valid number. * @throws MathException If the multiplier is not a valid number.
*
* @pure
*/ */
public function multipliedBy($that) : BigRational public function multipliedBy(BigNumber|int|float|string $that) : BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@ -275,11 +249,11 @@ final class BigRational extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. * @param BigNumber|int|float|string $that The divisor.
* *
* @return BigRational The result.
*
* @throws MathException If the divisor is not a valid number, or is zero. * @throws MathException If the divisor is not a valid number, or is zero.
*
* @pure
*/ */
public function dividedBy($that) : BigRational public function dividedBy(BigNumber|int|float|string $that) : BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@ -292,11 +266,9 @@ final class BigRational extends BigNumber
/** /**
* Returns this number exponentiated to the given value. * Returns this number exponentiated to the given value.
* *
* @param int $exponent The exponent.
*
* @return BigRational The result.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/ */
public function power(int $exponent) : BigRational public function power(int $exponent) : BigRational
{ {
@ -322,9 +294,9 @@ final class BigRational extends BigNumber
* *
* The reciprocal has the numerator and denominator swapped. * The reciprocal has the numerator and denominator swapped.
* *
* @return BigRational
*
* @throws DivisionByZeroException If the numerator is zero. * @throws DivisionByZeroException If the numerator is zero.
*
* @pure
*/ */
public function reciprocal() : BigRational public function reciprocal() : BigRational
{ {
@ -334,7 +306,7 @@ final class BigRational extends BigNumber
/** /**
* Returns the absolute value of this BigRational. * Returns the absolute value of this BigRational.
* *
* @return BigRational * @pure
*/ */
public function abs() : BigRational public function abs() : BigRational
{ {
@ -344,7 +316,7 @@ final class BigRational extends BigNumber
/** /**
* Returns the negated value of this BigRational. * Returns the negated value of this BigRational.
* *
* @return BigRational * @pure
*/ */
public function negated() : BigRational public function negated() : BigRational
{ {
@ -354,7 +326,7 @@ final class BigRational extends BigNumber
/** /**
* Returns the simplified value of this BigRational. * Returns the simplified value of this BigRational.
* *
* @return BigRational * @pure
*/ */
public function simplified() : BigRational public function simplified() : BigRational
{ {
@ -366,25 +338,19 @@ final class BigRational extends BigNumber
return new BigRational($numerator, $denominator, false); return new BigRational($numerator, $denominator, false);
} }
/** #[Override]
* {@inheritdoc} public function compareTo(BigNumber|int|float|string $that) : int
*/
public function compareTo($that) : int
{ {
return $this->minus($that)->getSign(); return $this->minus($that)->getSign();
} }
/** #[Override]
* {@inheritdoc}
*/
public function getSign() : int public function getSign() : int
{ {
return $this->numerator->getSign(); return $this->numerator->getSign();
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigInteger() : BigInteger public function toBigInteger() : BigInteger
{ {
$simplified = $this->simplified(); $simplified = $this->simplified();
@ -396,49 +362,38 @@ final class BigRational extends BigNumber
return $simplified->numerator; return $simplified->numerator;
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigDecimal() : BigDecimal public function toBigDecimal() : BigDecimal
{ {
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator); return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBigRational() : BigRational public function toBigRational() : BigRational
{ {
return $this; return $this;
} }
/** #[Override]
* {@inheritdoc} public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
*/
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{ {
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode); return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
} }
/** #[Override]
* {@inheritdoc}
*/
public function toInt() : int public function toInt() : int
{ {
return $this->toBigInteger()->toInt(); return $this->toBigInteger()->toInt();
} }
/** #[Override]
* {@inheritdoc}
*/
public function toFloat() : float public function toFloat() : float
{ {
return $this->numerator->toFloat() / $this->denominator->toFloat(); $simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
} }
/** #[Override]
* {@inheritdoc}
*/
public function __toString() : string public function __toString() : string
{ {
$numerator = (string) $this->numerator; $numerator = (string) $this->numerator;
@ -448,7 +403,7 @@ final class BigRational extends BigNumber
return $numerator; return $numerator;
} }
return $this->numerator . '/' . $this->denominator; return $numerator . '/' . $denominator;
} }
/** /**
@ -467,57 +422,20 @@ final class BigRational extends BigNumber
* This method is only here to allow unserializing the object and cannot be accessed directly. * This method is only here to allow unserializing the object and cannot be accessed directly.
* *
* @internal * @internal
* @psalm-suppress RedundantPropertyInitializationCheck
* *
* @param array{numerator: BigInteger, denominator: BigInteger} $data * @param array{numerator: BigInteger, denominator: BigInteger} $data
* *
* @return void
*
* @throws \LogicException * @throws \LogicException
*/ */
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->numerator)) { if (isset($this->numerator)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
} }
/** @phpstan-ignore deadCode.unreachable */
$this->numerator = $data['numerator']; $this->numerator = $data['numerator'];
$this->denominator = $data['denominator']; $this->denominator = $data['denominator'];
} }
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*
* @return string
*/
public function serialize() : string
{
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param string $value
*
* @return void
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->numerator)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$numerator, $denominator] = \explode('/', $value);
$this->numerator = BigInteger::of($numerator);
$this->denominator = BigInteger::of($denominator);
}
} }

View File

@ -7,12 +7,10 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when a division by zero occurs. * Exception thrown when a division by zero occurs.
*/ */
class DivisionByZeroException extends MathException final class DivisionByZeroException extends MathException
{ {
/** /**
* @return DivisionByZeroException * @pure
*
* @psalm-pure
*/ */
public static function divisionByZero() : DivisionByZeroException public static function divisionByZero() : DivisionByZeroException
{ {
@ -20,9 +18,7 @@ class DivisionByZeroException extends MathException
} }
/** /**
* @return DivisionByZeroException * @pure
*
* @psalm-pure
*/ */
public static function modulusMustNotBeZero() : DivisionByZeroException public static function modulusMustNotBeZero() : DivisionByZeroException
{ {
@ -30,9 +26,7 @@ class DivisionByZeroException extends MathException
} }
/** /**
* @return DivisionByZeroException * @pure
*
* @psalm-pure
*/ */
public static function denominatorMustNotBeZero() : DivisionByZeroException public static function denominatorMustNotBeZero() : DivisionByZeroException
{ {

View File

@ -9,14 +9,10 @@ use Brick\Math\BigInteger;
/** /**
* Exception thrown when an integer overflow occurs. * Exception thrown when an integer overflow occurs.
*/ */
class IntegerOverflowException extends MathException final class IntegerOverflowException extends MathException
{ {
/** /**
* @param BigInteger $value * @pure
*
* @return IntegerOverflowException
*
* @psalm-pure
*/ */
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
{ {

View File

@ -6,8 +6,6 @@ namespace Brick\Math\Exception;
/** /**
* Base class for all math exceptions. * Base class for all math exceptions.
*
* This class is abstract to ensure that only fine-grained exceptions are thrown throughout the code.
*/ */
class MathException extends \RuntimeException class MathException extends \RuntimeException
{ {

View File

@ -7,6 +7,6 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number. * Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/ */
class NegativeNumberException extends MathException final class NegativeNumberException extends MathException
{ {
} }

View File

@ -7,14 +7,23 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when attempting to create a number from a string with an invalid format. * Exception thrown when attempting to create a number from a string with an invalid format.
*/ */
class NumberFormatException extends MathException final class NumberFormatException extends MathException
{ {
/**
* @pure
*/
public static function invalidFormat(string $value) : self
{
return new self(\sprintf(
'The given value "%s" does not represent a valid number.',
$value,
));
}
/** /**
* @param string $char The failing character. * @param string $char The failing character.
* *
* @return NumberFormatException * @pure
*
* @psalm-pure
*/ */
public static function charNotInAlphabet(string $char) : self public static function charNotInAlphabet(string $char) : self
{ {
@ -30,6 +39,6 @@ class NumberFormatException extends MathException
$char = '"' . $char . '"'; $char = '"' . $char . '"';
} }
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char)); return new self(\sprintf('Char %s is not a valid character in the given alphabet.', $char));
} }
} }

View File

@ -7,12 +7,10 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when a number cannot be represented at the requested scale without rounding. * Exception thrown when a number cannot be represented at the requested scale without rounding.
*/ */
class RoundingNecessaryException extends MathException final class RoundingNecessaryException extends MathException
{ {
/** /**
* @return RoundingNecessaryException * @pure
*
* @psalm-pure
*/ */
public static function roundingNecessary() : RoundingNecessaryException public static function roundingNecessary() : RoundingNecessaryException
{ {

View File

@ -17,89 +17,25 @@ use Brick\Math\RoundingMode;
* All methods must return strings respecting this format, unless specified otherwise. * All methods must return strings respecting this format, unless specified otherwise.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
abstract class Calculator abstract readonly class Calculator
{ {
/** /**
* The maximum exponent value allowed for the pow() method. * The maximum exponent value allowed for the pow() method.
*/ */
public const MAX_POWER = 1000000; public const MAX_POWER = 1_000_000;
/** /**
* The alphabet for converting from and to base 2 to 36, lowercase. * The alphabet for converting from and to base 2 to 36, lowercase.
*/ */
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/**
* The Calculator instance in use.
*
* @var Calculator|null
*/
private static $instance;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: the autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
*
* @return void
*/
final public static function set(?Calculator $calculator) : void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* @return Calculator
*
* @psalm-pure
* @psalm-suppress ImpureStaticProperty
*/
final public static function get() : Calculator
{
if (self::$instance === null) {
/** @psalm-suppress ImpureMethodCall */
self::$instance = self::detect();
}
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @codeCoverageIgnore
*
* @return Calculator
*/
private static function detect() : Calculator
{
if (\extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (\extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
/** /**
* Extracts the sign & digits of the operands. * Extracts the sign & digits of the operands.
* *
* @param string $a The first operand.
* @param string $b The second operand.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits. * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*
* @pure
*/ */
final protected function init(string $a, string $b) : array final protected function init(string $a, string $b) : array
{ {
@ -115,9 +51,7 @@ abstract class Calculator
/** /**
* Returns the absolute value of a number. * Returns the absolute value of a number.
* *
* @param string $n The number. * @pure
*
* @return string The absolute value.
*/ */
final public function abs(string $n) : string final public function abs(string $n) : string
{ {
@ -127,9 +61,7 @@ abstract class Calculator
/** /**
* Negates a number. * Negates a number.
* *
* @param string $n The number. * @pure
*
* @return string The negated value.
*/ */
final public function neg(string $n) : string final public function neg(string $n) : string
{ {
@ -147,10 +79,11 @@ abstract class Calculator
/** /**
* Compares two numbers. * Compares two numbers.
* *
* @param string $a The first number. * Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
* @param string $b The second number.
* *
* @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number. * @return -1|0|1
*
* @pure
*/ */
final public function cmp(string $a, string $b) : int final public function cmp(string $a, string $b) : int
{ {
@ -181,30 +114,21 @@ abstract class Calculator
/** /**
* Adds two numbers. * Adds two numbers.
* *
* @param string $a The augend. * @pure
* @param string $b The addend.
*
* @return string The sum.
*/ */
abstract public function add(string $a, string $b) : string; abstract public function add(string $a, string $b) : string;
/** /**
* Subtracts two numbers. * Subtracts two numbers.
* *
* @param string $a The minuend. * @pure
* @param string $b The subtrahend.
*
* @return string The difference.
*/ */
abstract public function sub(string $a, string $b) : string; abstract public function sub(string $a, string $b) : string;
/** /**
* Multiplies two numbers. * Multiplies two numbers.
* *
* @param string $a The multiplicand. * @pure
* @param string $b The multiplier.
*
* @return string The product.
*/ */
abstract public function mul(string $a, string $b) : string; abstract public function mul(string $a, string $b) : string;
@ -215,6 +139,8 @@ abstract class Calculator
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* *
* @return string The quotient. * @return string The quotient.
*
* @pure
*/ */
abstract public function divQ(string $a, string $b) : string; abstract public function divQ(string $a, string $b) : string;
@ -225,6 +151,8 @@ abstract class Calculator
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* *
* @return string The remainder. * @return string The remainder.
*
* @pure
*/ */
abstract public function divR(string $a, string $b) : string; abstract public function divR(string $a, string $b) : string;
@ -234,7 +162,9 @@ abstract class Calculator
* @param string $a The dividend. * @param string $a The dividend.
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* *
* @return string[] An array containing the quotient and remainder. * @return array{string, string} An array containing the quotient and remainder.
*
* @pure
*/ */
abstract public function divQR(string $a, string $b) : array; abstract public function divQR(string $a, string $b) : array;
@ -245,14 +175,15 @@ abstract class Calculator
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER. * @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
* *
* @return string The power. * @return string The power.
*
* @pure
*/ */
abstract public function pow(string $a, int $e) : string; abstract public function pow(string $a, int $e) : string;
/** /**
* @param string $a
* @param string $b The modulus; must not be zero. * @param string $b The modulus; must not be zero.
* *
* @return string * @pure
*/ */
public function mod(string $a, string $b) : string public function mod(string $a, string $b) : string
{ {
@ -266,10 +197,9 @@ abstract class Calculator
* *
* This method can be overridden by the concrete implementation if the underlying library has built-in support. * This method can be overridden by the concrete implementation if the underlying library has built-in support.
* *
* @param string $x
* @param string $m The modulus; must not be negative or zero. * @param string $m The modulus; must not be negative or zero.
* *
* @return string|null * @pure
*/ */
public function modInverse(string $x, string $m) : ?string public function modInverse(string $x, string $m) : ?string
{ {
@ -283,9 +213,7 @@ abstract class Calculator
$modVal = $this->mod($x, $m); $modVal = $this->mod($x, $m);
} }
$x = '0'; [$g, $x] = $this->gcdExtended($modVal, $m);
$y = '0';
$g = $this->gcdExtended($modVal, $m, $x, $y);
if ($g !== '1') { if ($g !== '1') {
return null; return null;
@ -301,7 +229,7 @@ abstract class Calculator
* @param string $exp The exponent; must be positive or zero. * @param string $exp The exponent; must be positive or zero.
* @param string $mod The modulus; must be strictly positive. * @param string $mod The modulus; must be strictly positive.
* *
* @return string The power. * @pure
*/ */
abstract public function modPow(string $base, string $exp, string $mod) : string; abstract public function modPow(string $base, string $exp, string $mod) : string;
@ -311,10 +239,9 @@ abstract class Calculator
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for GCD calculations. * has built-in support for GCD calculations.
* *
* @param string $a The first number.
* @param string $b The second number.
*
* @return string The GCD, always positive, or zero if both arguments are zero. * @return string The GCD, always positive, or zero if both arguments are zero.
*
* @pure
*/ */
public function gcd(string $a, string $b) : string public function gcd(string $a, string $b) : string
{ {
@ -329,24 +256,23 @@ abstract class Calculator
return $this->gcd($b, $this->divR($a, $b)); return $this->gcd($b, $this->divR($a, $b));
} }
private function gcdExtended(string $a, string $b, string &$x, string &$y) : string /**
* @return array{string, string, string} GCD, X, Y
*
* @pure
*/
private function gcdExtended(string $a, string $b) : array
{ {
if ($a === '0') { if ($a === '0') {
$x = '0'; return [$b, '0', '1'];
$y = '1';
return $b;
} }
$x1 = '0'; [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
$y1 = '0';
$gcd = $this->gcdExtended($this->mod($b, $a), $a, $x1, $y1);
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1)); $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
$y = $x1; $y = $x1;
return $gcd; return [$gcd, $x, $y];
} }
/** /**
@ -355,9 +281,7 @@ abstract class Calculator
* The result is the largest x such that n. * The result is the largest x such that n.
* The input MUST NOT be negative. * The input MUST NOT be negative.
* *
* @param string $n The number. * @pure
*
* @return string The square root.
*/ */
abstract public function sqrt(string $n) : string; abstract public function sqrt(string $n) : string;
@ -371,6 +295,8 @@ abstract class Calculator
* @param int $base The base of the number, validated from 2 to 36. * @param int $base The base of the number, validated from 2 to 36.
* *
* @return string The converted number, following the Calculator conventions. * @return string The converted number, following the Calculator conventions.
*
* @pure
*/ */
public function fromBase(string $number, int $base) : string public function fromBase(string $number, int $base) : string
{ {
@ -387,6 +313,8 @@ abstract class Calculator
* @param int $base The base to convert to, validated from 2 to 36. * @param int $base The base to convert to, validated from 2 to 36.
* *
* @return string The converted number, lowercase. * @return string The converted number, lowercase.
*
* @pure
*/ */
public function toBase(string $number, int $base) : string public function toBase(string $number, int $base) : string
{ {
@ -414,6 +342,8 @@ abstract class Calculator
* @param int $base The base of the number, validated from 2 to alphabet length. * @param int $base The base of the number, validated from 2 to alphabet length.
* *
* @return string The number in base 10, following the Calculator conventions. * @return string The number in base 10, following the Calculator conventions.
*
* @pure
*/ */
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
{ {
@ -460,6 +390,8 @@ abstract class Calculator
* @param int $base The base to convert to, validated from 2 to alphabet length. * @param int $base The base to convert to, validated from 2 to alphabet length.
* *
* @return string The converted number in the given alphabet. * @return string The converted number in the given alphabet.
*
* @pure
*/ */
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
{ {
@ -485,16 +417,15 @@ abstract class Calculator
* *
* Rounding is performed when the remainder of the division is not zero. * Rounding is performed when the remainder of the division is not zero.
* *
* @param string $a The dividend. * @param string $a The dividend.
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* @param int $roundingMode The rounding mode. * @param RoundingMode $roundingMode The rounding mode.
* *
* @return string
*
* @throws \InvalidArgumentException If the rounding mode is invalid.
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary. * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
*
* @pure
*/ */
final public function divRound(string $a, string $b, int $roundingMode) : string final public function divRound(string $a, string $b, RoundingMode $roundingMode) : string
{ {
[$quotient, $remainder] = $this->divQR($a, $b); [$quotient, $remainder] = $this->divQR($a, $b);
@ -553,9 +484,6 @@ abstract class Calculator
$lastDigitIsEven = ($lastDigit % 2 === 0); $lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break; break;
default:
throw new \InvalidArgumentException('Invalid rounding mode.');
} }
if ($increment) { if ($increment) {
@ -571,10 +499,7 @@ abstract class Calculator
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations. * has built-in support for bitwise operations.
* *
* @param string $a * @pure
* @param string $b
*
* @return string
*/ */
public function and(string $a, string $b) : string public function and(string $a, string $b) : string
{ {
@ -587,10 +512,7 @@ abstract class Calculator
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations. * has built-in support for bitwise operations.
* *
* @param string $a * @pure
* @param string $b
*
* @return string
*/ */
public function or(string $a, string $b) : string public function or(string $a, string $b) : string
{ {
@ -603,10 +525,7 @@ abstract class Calculator
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations. * has built-in support for bitwise operations.
* *
* @param string $a * @pure
* @param string $b
*
* @return string
*/ */
public function xor(string $a, string $b) : string public function xor(string $a, string $b) : string
{ {
@ -616,11 +535,11 @@ abstract class Calculator
/** /**
* Performs a bitwise operation on a decimal number. * Performs a bitwise operation on a decimal number.
* *
* @param string $operator The operator to use, must be "and", "or" or "xor". * @param 'and'|'or'|'xor' $operator The operator to use.
* @param string $a The left operand. * @param string $a The left operand.
* @param string $b The right operand. * @param string $b The right operand.
* *
* @return string * @pure
*/ */
private function bitwise(string $operator, string $a, string $b) : string private function bitwise(string $operator, string $a, string $b) : string
{ {
@ -645,27 +564,17 @@ abstract class Calculator
$bBin = $this->twosComplement($bBin); $bBin = $this->twosComplement($bBin);
} }
switch ($operator) { $value = match ($operator) {
case 'and': 'and' => $aBin & $bBin,
$value = $aBin & $bBin; 'or' => $aBin | $bBin,
$negative = ($aNeg and $bNeg); 'xor' => $aBin ^ $bBin,
break; };
case 'or': $negative = match ($operator) {
$value = $aBin | $bBin; 'and' => $aNeg and $bNeg,
$negative = ($aNeg or $bNeg); 'or' => $aNeg or $bNeg,
break; 'xor' => $aNeg xor $bNeg,
};
case 'xor':
$value = $aBin ^ $bBin;
$negative = ($aNeg xor $bNeg);
break;
// @codeCoverageIgnoreStart
default:
throw new \InvalidArgumentException('Invalid bitwise operator.');
// @codeCoverageIgnoreEnd
}
if ($negative) { if ($negative) {
$value = $this->twosComplement($value); $value = $this->twosComplement($value);
@ -679,7 +588,7 @@ abstract class Calculator
/** /**
* @param string $number A positive, binary number. * @param string $number A positive, binary number.
* *
* @return string * @pure
*/ */
private function twosComplement(string $number) : string private function twosComplement(string $number) : string
{ {
@ -710,7 +619,7 @@ abstract class Calculator
* *
* @param string $number The number to convert, positive or zero, only digits. * @param string $number The number to convert, positive or zero, only digits.
* *
* @return string * @pure
*/ */
private function toBinary(string $number) : string private function toBinary(string $number) : string
{ {
@ -729,7 +638,7 @@ abstract class Calculator
* *
* @param string $bytes The bytes representing the number. * @param string $bytes The bytes representing the number.
* *
* @return string * @pure
*/ */
private function toDecimal(string $bytes) : string private function toDecimal(string $bytes) : string
{ {

View File

@ -5,110 +5,67 @@ declare(strict_types=1);
namespace Brick\Math\Internal\Calculator; namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Override;
/** /**
* Calculator implementation built around the bcmath library. * Calculator implementation built around the bcmath library.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
class BcMathCalculator extends Calculator final readonly class BcMathCalculator extends Calculator
{ {
/** #[Override]
* {@inheritdoc}
*/
public function add(string $a, string $b) : string public function add(string $a, string $b) : string
{ {
return \bcadd($a, $b, 0); return \bcadd($a, $b, 0);
} }
/** #[Override]
* {@inheritdoc}
*/
public function sub(string $a, string $b) : string public function sub(string $a, string $b) : string
{ {
return \bcsub($a, $b, 0); return \bcsub($a, $b, 0);
} }
/** #[Override]
* {@inheritdoc}
*/
public function mul(string $a, string $b) : string public function mul(string $a, string $b) : string
{ {
return \bcmul($a, $b, 0); return \bcmul($a, $b, 0);
} }
/** #[Override]
* {@inheritdoc}
*
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function divQ(string $a, string $b) : string public function divQ(string $a, string $b) : string
{ {
return \bcdiv($a, $b, 0); return \bcdiv($a, $b, 0);
} }
/** #[Override]
* {@inheritdoc}
*
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function divR(string $a, string $b) : string public function divR(string $a, string $b) : string
{ {
if (version_compare(PHP_VERSION, '7.2') >= 0) { return \bcmod($a, $b, 0);
return \bcmod($a, $b, 0);
}
return \bcmod($a, $b);
} }
/** #[Override]
* {@inheritdoc}
*/
public function divQR(string $a, string $b) : array public function divQR(string $a, string $b) : array
{ {
$q = \bcdiv($a, $b, 0); $q = \bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0);
if (version_compare(PHP_VERSION, '7.2') >= 0) {
$r = \bcmod($a, $b, 0);
} else {
$r = \bcmod($a, $b);
}
assert($q !== null);
assert($r !== null);
return [$q, $r]; return [$q, $r];
} }
/** #[Override]
* {@inheritdoc}
*/
public function pow(string $a, int $e) : string public function pow(string $a, int $e) : string
{ {
return \bcpow($a, (string) $e, 0); return \bcpow($a, (string) $e, 0);
} }
/** #[Override]
* {@inheritdoc}
*
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function modPow(string $base, string $exp, string $mod) : string public function modPow(string $base, string $exp, string $mod) : string
{ {
return \bcpowmod($base, $exp, $mod, 0); return \bcpowmod($base, $exp, $mod, 0);
} }
/** #[Override]
* {@inheritDoc}
*
* @psalm-suppress NullableReturnStatement
* @psalm-suppress InvalidNullableReturnType
*/
public function sqrt(string $n) : string public function sqrt(string $n) : string
{ {
return \bcsqrt($n, 0); return \bcsqrt($n, 0);

View File

@ -5,80 +5,68 @@ declare(strict_types=1);
namespace Brick\Math\Internal\Calculator; namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use GMP;
use Override;
/** /**
* Calculator implementation built around the GMP library. * Calculator implementation built around the GMP library.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
class GmpCalculator extends Calculator final readonly class GmpCalculator extends Calculator
{ {
/** #[Override]
* {@inheritdoc}
*/
public function add(string $a, string $b) : string public function add(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_add($a, $b)); return \gmp_strval(\gmp_add($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function sub(string $a, string $b) : string public function sub(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_sub($a, $b)); return \gmp_strval(\gmp_sub($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function mul(string $a, string $b) : string public function mul(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_mul($a, $b)); return \gmp_strval(\gmp_mul($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function divQ(string $a, string $b) : string public function divQ(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_div_q($a, $b)); return \gmp_strval(\gmp_div_q($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function divR(string $a, string $b) : string public function divR(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_div_r($a, $b)); return \gmp_strval(\gmp_div_r($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function divQR(string $a, string $b) : array public function divQR(string $a, string $b) : array
{ {
[$q, $r] = \gmp_div_qr($a, $b); [$q, $r] = \gmp_div_qr($a, $b);
/**
* @var GMP $q
* @var GMP $r
*/
return [ return [
\gmp_strval($q), \gmp_strval($q),
\gmp_strval($r) \gmp_strval($r)
]; ];
} }
/** #[Override]
* {@inheritdoc}
*/
public function pow(string $a, int $e) : string public function pow(string $a, int $e) : string
{ {
return \gmp_strval(\gmp_pow($a, $e)); return \gmp_strval(\gmp_pow($a, $e));
} }
/** #[Override]
* {@inheritdoc}
*/
public function modInverse(string $x, string $m) : ?string public function modInverse(string $x, string $m) : ?string
{ {
$result = \gmp_invert($x, $m); $result = \gmp_invert($x, $m);
@ -90,65 +78,49 @@ class GmpCalculator extends Calculator
return \gmp_strval($result); return \gmp_strval($result);
} }
/** #[Override]
* {@inheritdoc}
*/
public function modPow(string $base, string $exp, string $mod) : string public function modPow(string $base, string $exp, string $mod) : string
{ {
return \gmp_strval(\gmp_powm($base, $exp, $mod)); return \gmp_strval(\gmp_powm($base, $exp, $mod));
} }
/** #[Override]
* {@inheritdoc}
*/
public function gcd(string $a, string $b) : string public function gcd(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_gcd($a, $b)); return \gmp_strval(\gmp_gcd($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function fromBase(string $number, int $base) : string public function fromBase(string $number, int $base) : string
{ {
return \gmp_strval(\gmp_init($number, $base)); return \gmp_strval(\gmp_init($number, $base));
} }
/** #[Override]
* {@inheritdoc}
*/
public function toBase(string $number, int $base) : string public function toBase(string $number, int $base) : string
{ {
return \gmp_strval($number, $base); return \gmp_strval($number, $base);
} }
/** #[Override]
* {@inheritdoc}
*/
public function and(string $a, string $b) : string public function and(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_and($a, $b)); return \gmp_strval(\gmp_and($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function or(string $a, string $b) : string public function or(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_or($a, $b)); return \gmp_strval(\gmp_or($a, $b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function xor(string $a, string $b) : string public function xor(string $a, string $b) : string
{ {
return \gmp_strval(\gmp_xor($a, $b)); return \gmp_strval(\gmp_xor($a, $b));
} }
/** #[Override]
* {@inheritDoc}
*/
public function sqrt(string $n) : string public function sqrt(string $n) : string
{ {
return \gmp_strval(\gmp_sqrt($n)); return \gmp_strval(\gmp_sqrt($n));

View File

@ -5,57 +5,43 @@ declare(strict_types=1);
namespace Brick\Math\Internal\Calculator; namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Override;
/** /**
* Calculator implementation using only native PHP code. * Calculator implementation using only native PHP code.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
class NativeCalculator extends Calculator final readonly class NativeCalculator extends Calculator
{ {
/** /**
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow. * The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
* For multiplication, this represents the max sum of the lengths of both operands. * For multiplication, this represents the max sum of the lengths of both operands.
* *
* For addition, it is assumed that an extra digit can hold a carry (1) without overflowing. * In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry) * Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry) * 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
*
* @var int
*/ */
private $maxDigits; private int $maxDigits;
/** /**
* Class constructor. * @pure
*
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
public function __construct() public function __construct()
{ {
switch (PHP_INT_SIZE) { $this->maxDigits = match (PHP_INT_SIZE) {
case 4: 4 => 9,
$this->maxDigits = 9; 8 => 18,
break; };
case 8:
$this->maxDigits = 18;
break;
default:
throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
}
} }
/** #[Override]
* {@inheritdoc}
*/
public function add(string $a, string $b) : string public function add(string $a, string $b) : string
{ {
/** /**
* @psalm-var numeric-string $a * @var numeric-string $a
* @psalm-var numeric-string $b * @var numeric-string $b
*/ */
$result = $a + $b; $result = $a + $b;
@ -82,22 +68,18 @@ class NativeCalculator extends Calculator
return $result; return $result;
} }
/** #[Override]
* {@inheritdoc}
*/
public function sub(string $a, string $b) : string public function sub(string $a, string $b) : string
{ {
return $this->add($a, $this->neg($b)); return $this->add($a, $this->neg($b));
} }
/** #[Override]
* {@inheritdoc}
*/
public function mul(string $a, string $b) : string public function mul(string $a, string $b) : string
{ {
/** /**
* @psalm-var numeric-string $a * @var numeric-string $a
* @psalm-var numeric-string $b * @var numeric-string $b
*/ */
$result = $a * $b; $result = $a * $b;
@ -136,25 +118,19 @@ class NativeCalculator extends Calculator
return $result; return $result;
} }
/** #[Override]
* {@inheritdoc}
*/
public function divQ(string $a, string $b) : string public function divQ(string $a, string $b) : string
{ {
return $this->divQR($a, $b)[0]; return $this->divQR($a, $b)[0];
} }
/** #[Override]
* {@inheritdoc}
*/
public function divR(string $a, string $b): string public function divR(string $a, string $b): string
{ {
return $this->divQR($a, $b)[1]; return $this->divQR($a, $b)[1];
} }
/** #[Override]
* {@inheritdoc}
*/
public function divQR(string $a, string $b) : array public function divQR(string $a, string $b) : array
{ {
if ($a === '0') { if ($a === '0') {
@ -173,20 +149,18 @@ class NativeCalculator extends Calculator
return [$this->neg($a), '0']; return [$this->neg($a), '0'];
} }
/** @psalm-var numeric-string $a */ /** @var numeric-string $a */
$na = $a * 1; // cast to number $na = $a * 1; // cast to number
if (is_int($na)) { if (is_int($na)) {
/** @psalm-var numeric-string $b */ /** @var numeric-string $b */
$nb = $b * 1; $nb = $b * 1;
if (is_int($nb)) { if (is_int($nb)) {
// the only division that may overflow is PHP_INT_MIN / -1, // the only division that may overflow is PHP_INT_MIN / -1,
// which cannot happen here as we've already handled a divisor of -1 above. // which cannot happen here as we've already handled a divisor of -1 above.
$q = intdiv($na, $nb);
$r = $na % $nb; $r = $na % $nb;
$q = ($na - $r) / $nb;
assert(is_int($q));
return [ return [
(string) $q, (string) $q,
@ -210,9 +184,7 @@ class NativeCalculator extends Calculator
return [$q, $r]; return [$q, $r];
} }
/** #[Override]
* {@inheritdoc}
*/
public function pow(string $a, int $e) : string public function pow(string $a, int $e) : string
{ {
if ($e === 0) { if ($e === 0) {
@ -228,7 +200,6 @@ class NativeCalculator extends Calculator
$aa = $this->mul($a, $a); $aa = $this->mul($a, $a);
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
$result = $this->pow($aa, $e / 2); $result = $this->pow($aa, $e / 2);
if ($odd === 1) { if ($odd === 1) {
@ -240,9 +211,8 @@ class NativeCalculator extends Calculator
/** /**
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/ * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
*
* {@inheritdoc}
*/ */
#[Override]
public function modPow(string $base, string $exp, string $mod) : string public function modPow(string $base, string $exp, string $mod) : string
{ {
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0) // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
@ -276,9 +246,8 @@ class NativeCalculator extends Calculator
/** /**
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
*
* {@inheritDoc}
*/ */
#[Override]
public function sqrt(string $n) : string public function sqrt(string $n) : string
{ {
if ($n === '0') { if ($n === '0') {
@ -307,10 +276,7 @@ class NativeCalculator extends Calculator
/** /**
* Performs the addition of two non-signed large integers. * Performs the addition of two non-signed large integers.
* *
* @param string $a The first operand. * @pure
* @param string $b The second operand.
*
* @return string
*/ */
private function doAdd(string $a, string $b) : string private function doAdd(string $a, string $b) : string
{ {
@ -324,14 +290,13 @@ class NativeCalculator extends Calculator
if ($i < 0) { if ($i < 0) {
$blockLength += $i; $blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0; $i = 0;
} }
/** @psalm-var numeric-string $blockA */ /** @var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength); $blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */ /** @var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength); $blockB = \substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry); $sum = (string) ($blockA + $blockB + $carry);
@ -364,10 +329,7 @@ class NativeCalculator extends Calculator
/** /**
* Performs the subtraction of two non-signed large integers. * Performs the subtraction of two non-signed large integers.
* *
* @param string $a The first operand. * @pure
* @param string $b The second operand.
*
* @return string
*/ */
private function doSub(string $a, string $b) : string private function doSub(string $a, string $b) : string
{ {
@ -398,14 +360,13 @@ class NativeCalculator extends Calculator
if ($i < 0) { if ($i < 0) {
$blockLength += $i; $blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0; $i = 0;
} }
/** @psalm-var numeric-string $blockA */ /** @var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength); $blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */ /** @var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength); $blockB = \substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry; $sum = $blockA - $blockB - $carry;
@ -446,10 +407,7 @@ class NativeCalculator extends Calculator
/** /**
* Performs the multiplication of two non-signed large integers. * Performs the multiplication of two non-signed large integers.
* *
* @param string $a The first operand. * @pure
* @param string $b The second operand.
*
* @return string
*/ */
private function doMul(string $a, string $b) : string private function doMul(string $a, string $b) : string
{ {
@ -466,7 +424,6 @@ class NativeCalculator extends Calculator
if ($i < 0) { if ($i < 0) {
$blockALength += $i; $blockALength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0; $i = 0;
} }
@ -480,7 +437,6 @@ class NativeCalculator extends Calculator
if ($j < 0) { if ($j < 0) {
$blockBLength += $j; $blockBLength += $j;
/** @psalm-suppress LoopInvalidation */
$j = 0; $j = 0;
} }
@ -522,10 +478,9 @@ class NativeCalculator extends Calculator
/** /**
* Performs the division of two non-signed large integers. * Performs the division of two non-signed large integers.
* *
* @param string $a The first operand.
* @param string $b The second operand.
*
* @return string[] The quotient and remainder. * @return string[] The quotient and remainder.
*
* @pure
*/ */
private function doDiv(string $a, string $b) : array private function doDiv(string $a, string $b) : array
{ {
@ -544,6 +499,22 @@ class NativeCalculator extends Calculator
$r = $a; // remainder $r = $a; // remainder
$z = $y; // focus length, always $y or $y+1 $z = $y; // focus length, always $y or $y+1
/** @var numeric-string $b */
$nb = $b * 1; // cast to number
// performance optimization in cases where the remainder will never cause int overflow
if (is_int(($nb - 1) * 10 + 9)) {
$r = (int) \substr($a, 0, $z - 1);
for ($i = $z - 1; $i < $x; $i++) {
$n = $r * 10 + (int) $a[$i];
/** @var int $nb */
$q .= \intdiv($n, $nb);
$r = $n % $nb;
}
return [\ltrim($q, '0') ?: '0', (string) $r];
}
for (;;) { for (;;) {
$focus = \substr($a, 0, $z); $focus = \substr($a, 0, $z);
@ -583,10 +554,9 @@ class NativeCalculator extends Calculator
/** /**
* Compares two non-signed large numbers. * Compares two non-signed large numbers.
* *
* @param string $a The first operand. * @return -1|0|1
* @param string $b The second operand.
* *
* @return int [-1, 0, 1] * @pure
*/ */
private function doCmp(string $a, string $b) : int private function doCmp(string $a, string $b) : int
{ {
@ -599,7 +569,7 @@ class NativeCalculator extends Calculator
return $cmp; return $cmp;
} }
return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1] return \strcmp($a, $b) <=> 0; // enforce -1|0|1
} }
/** /**
@ -607,10 +577,9 @@ class NativeCalculator extends Calculator
* *
* The numbers must only consist of digits, without leading minus sign. * The numbers must only consist of digits, without leading minus sign.
* *
* @param string $a The first operand.
* @param string $b The second operand.
*
* @return array{string, string, int} * @return array{string, string, int}
*
* @pure
*/ */
private function pad(string $a, string $b) : array private function pad(string $a, string $b) : array
{ {

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use function extension_loaded;
/**
* Stores the current Calculator instance used by BigNumber classes.
*
* @internal
*/
final class CalculatorRegistry
{
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or null to revert to autodetect.
*/
final public static function set(?Calculator $calculator) : void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* Note: even though this method is not technically pure, it is considered pure when used in a normal context, when
* only relying on autodetect.
*
* @pure
*/
final public static function get() : Calculator
{
/** @phpstan-ignore impure.staticPropertyAccess */
if (self::$instance === null) {
/** @phpstan-ignore impure.propertyAssign */
self::$instance = self::detect();
}
/** @phpstan-ignore impure.staticPropertyAccess */
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @pure
* @codeCoverageIgnore
*/
private static function detect() : Calculator
{
if (extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
}

View File

@ -13,24 +13,15 @@ namespace Brick\Math;
* regardless the digits' contribution to the value of the number. In other words, considered * regardless the digits' contribution to the value of the number. In other words, considered
* as a numerical value, the discarded fraction could have an absolute value greater than one. * as a numerical value, the discarded fraction could have an absolute value greater than one.
*/ */
final class RoundingMode enum RoundingMode
{ {
/**
* Private constructor. This class is not instantiable.
*
* @codeCoverageIgnore
*/
private function __construct()
{
}
/** /**
* Asserts that the requested operation has an exact result, hence no rounding is necessary. * Asserts that the requested operation has an exact result, hence no rounding is necessary.
* *
* If this rounding mode is specified on an operation that yields a result that * If this rounding mode is specified on an operation that yields a result that
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown. * cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
*/ */
public const UNNECESSARY = 0; case UNNECESSARY;
/** /**
* Rounds away from zero. * Rounds away from zero.
@ -38,7 +29,7 @@ final class RoundingMode
* Always increments the digit prior to a nonzero discarded fraction. * Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value. * Note that this rounding mode never decreases the magnitude of the calculated value.
*/ */
public const UP = 1; case UP;
/** /**
* Rounds towards zero. * Rounds towards zero.
@ -46,7 +37,7 @@ final class RoundingMode
* Never increments the digit prior to a discarded fraction (i.e., truncates). * Never increments the digit prior to a discarded fraction (i.e., truncates).
* Note that this rounding mode never increases the magnitude of the calculated value. * Note that this rounding mode never increases the magnitude of the calculated value.
*/ */
public const DOWN = 2; case DOWN;
/** /**
* Rounds towards positive infinity. * Rounds towards positive infinity.
@ -54,7 +45,7 @@ final class RoundingMode
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN. * If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
* Note that this rounding mode never decreases the calculated value. * Note that this rounding mode never decreases the calculated value.
*/ */
public const CEILING = 3; case CEILING;
/** /**
* Rounds towards negative infinity. * Rounds towards negative infinity.
@ -62,7 +53,7 @@ final class RoundingMode
* If the result is positive, behave as for DOWN; if negative, behave as for UP. * If the result is positive, behave as for DOWN; if negative, behave as for UP.
* Note that this rounding mode never increases the calculated value. * Note that this rounding mode never increases the calculated value.
*/ */
public const FLOOR = 4; case FLOOR;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
@ -70,28 +61,28 @@ final class RoundingMode
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN. * Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
* Note that this is the rounding mode commonly taught at school. * Note that this is the rounding mode commonly taught at school.
*/ */
public const HALF_UP = 5; case HALF_UP;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
* *
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN. * Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
*/ */
public const HALF_DOWN = 6; case HALF_DOWN;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
* *
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN. * If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
*/ */
public const HALF_CEILING = 7; case HALF_CEILING;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
* *
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP. * If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
*/ */
public const HALF_FLOOR = 8; case HALF_FLOOR;
/** /**
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor. * Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
@ -103,5 +94,5 @@ final class RoundingMode
* cumulative error when applied repeatedly over a sequence of calculations. * cumulative error when applied repeatedly over a sequence of calculations.
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA. * It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
*/ */
public const HALF_EVEN = 9; case HALF_EVEN;
} }

View File

@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/ */
class InstalledVersions class InstalledVersions
{ {
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/** /**
* @var mixed[]|null * @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/ */
private static $installed; private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/** /**
* @var bool|null * @var bool|null
*/ */
@ -309,6 +320,24 @@ class InstalledVersions
{ {
self::$installed = $data; self::$installed = $data;
self::$installedByVendor = array(); self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
} }
/** /**
@ -322,19 +351,27 @@ class InstalledVersions
} }
$installed = array(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php'; $required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required; self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $installed[] = $required;
self::$installed = $installed[count($installed) - 1]; if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
} }
} }
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
} }
} }
@ -350,7 +387,7 @@ class InstalledVersions
} }
} }
if (self::$installed !== array()) { if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed; $installed[] = self::$installed;
} }

View File

@ -6,11 +6,8 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'Deprecated' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'ReflectionConstant' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
'lessc' => $vendorDir . '/wikimedia/less.php/lessc.inc.php', 'lessc' => $vendorDir . '/wikimedia/less.php/lessc.inc.php',
); );

View File

@ -8,9 +8,8 @@ $baseDir = dirname($vendorDir);
return array( return array(
'a2c78434f64e5f5ed402f42eee19c025' => $vendorDir . '/ipl/stdlib/src/functions_include.php', 'a2c78434f64e5f5ed402f42eee19c025' => $vendorDir . '/ipl/stdlib/src/functions_include.php',
'6076de347104821999fcfc82c8f19bc5' => $vendorDir . '/ipl/i18n/src/functions_include.php', '6076de347104821999fcfc82c8f19bc5' => $vendorDir . '/ipl/i18n/src/functions_include.php',
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php', 'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php', 'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
'8e4ccce73649a2b516ec3b4571432da5' => $vendorDir . '/ipl/scheduler/src/register_cron_aliases.php', '8e4ccce73649a2b516ec3b4571432da5' => $vendorDir . '/ipl/scheduler/src/register_cron_aliases.php',

View File

@ -14,21 +14,18 @@ return array(
'ipl\\Orm\\' => array($vendorDir . '/ipl/orm/src'), 'ipl\\Orm\\' => array($vendorDir . '/ipl/orm/src'),
'ipl\\I18n\\' => array($vendorDir . '/ipl/i18n/src'), 'ipl\\I18n\\' => array($vendorDir . '/ipl/i18n/src'),
'ipl\\Html\\' => array($vendorDir . '/ipl/html/src'), 'ipl\\Html\\' => array($vendorDir . '/ipl/html/src'),
'cweagans\\Composer\\' => array($vendorDir . '/cweagans/composer-patches/src'), 'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Recurr\\' => array($vendorDir . '/simshaun/recurr/src/Recurr'), 'Recurr\\' => array($vendorDir . '/simshaun/recurr/src/Recurr'),
'React\\Promise\\' => array($vendorDir . '/react/promise/src'), 'React\\Promise\\' => array($vendorDir . '/react/promise/src'),
'React\\EventLoop\\' => array($vendorDir . '/react/event-loop/src'), 'React\\EventLoop\\' => array($vendorDir . '/react/event-loop/src'),
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'), 'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'), 'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'Evenement\\' => array($vendorDir . '/evenement/evenement/src'), 'Evenement\\' => array($vendorDir . '/evenement/evenement/src'),
'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'), 'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
'Doctrine\\Common\\Collections\\' => array($vendorDir . '/doctrine/collections/lib/Doctrine/Common/Collections'), 'Doctrine\\Common\\Collections\\' => array($vendorDir . '/doctrine/collections/src'),
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'), 'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'), 'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
); );

View File

@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer // autoload_real.php @generated by Composer
class ComposerAutoloaderInit20d4022bc196691807f55d4a47c06474 class ComposerAutoloaderInit7a1692c86b6fc70eaaf43c4bee3673aa
{ {
private static $loader; private static $loader;
@ -24,16 +24,16 @@ class ComposerAutoloaderInit20d4022bc196691807f55d4a47c06474
require __DIR__ . '/platform_check.php'; require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit20d4022bc196691807f55d4a47c06474', 'loadClassLoader'), true, true); spl_autoload_register(array('ComposerAutoloaderInit7a1692c86b6fc70eaaf43c4bee3673aa', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit20d4022bc196691807f55d4a47c06474', 'loadClassLoader')); spl_autoload_unregister(array('ComposerAutoloaderInit7a1692c86b6fc70eaaf43c4bee3673aa', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php'; require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit20d4022bc196691807f55d4a47c06474::getInitializer($loader)); call_user_func(\Composer\Autoload\ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa::getInitializer($loader));
$loader->register(true); $loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit20d4022bc196691807f55d4a47c06474::$files; $filesToLoad = \Composer\Autoload\ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) { $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;

View File

@ -4,14 +4,13 @@
namespace Composer\Autoload; namespace Composer\Autoload;
class ComposerStaticInit20d4022bc196691807f55d4a47c06474 class ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa
{ {
public static $files = array ( public static $files = array (
'a2c78434f64e5f5ed402f42eee19c025' => __DIR__ . '/..' . '/ipl/stdlib/src/functions_include.php', 'a2c78434f64e5f5ed402f42eee19c025' => __DIR__ . '/..' . '/ipl/stdlib/src/functions_include.php',
'6076de347104821999fcfc82c8f19bc5' => __DIR__ . '/..' . '/ipl/i18n/src/functions_include.php', '6076de347104821999fcfc82c8f19bc5' => __DIR__ . '/..' . '/ipl/i18n/src/functions_include.php',
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php', 'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php', 'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php',
'8e4ccce73649a2b516ec3b4571432da5' => __DIR__ . '/..' . '/ipl/scheduler/src/register_cron_aliases.php', '8e4ccce73649a2b516ec3b4571432da5' => __DIR__ . '/..' . '/ipl/scheduler/src/register_cron_aliases.php',
@ -29,18 +28,9 @@ class ComposerStaticInit20d4022bc196691807f55d4a47c06474
'ipl\\I18n\\' => 9, 'ipl\\I18n\\' => 9,
'ipl\\Html\\' => 9, 'ipl\\Html\\' => 9,
), ),
'c' =>
array (
'cweagans\\Composer\\' => 18,
),
'W' =>
array (
'Webmozart\\Assert\\' => 17,
),
'S' => 'S' =>
array ( array (
'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Php84\\' => 23,
'Symfony\\Polyfill\\Ctype\\' => 23,
), ),
'R' => 'R' =>
array ( array (
@ -111,21 +101,9 @@ class ComposerStaticInit20d4022bc196691807f55d4a47c06474
array ( array (
0 => __DIR__ . '/..' . '/ipl/html/src', 0 => __DIR__ . '/..' . '/ipl/html/src',
), ),
'cweagans\\Composer\\' => 'Symfony\\Polyfill\\Php84\\' =>
array ( array (
0 => __DIR__ . '/..' . '/cweagans/composer-patches/src', 0 => __DIR__ . '/..' . '/symfony/polyfill-php84',
),
'Webmozart\\Assert\\' =>
array (
0 => __DIR__ . '/..' . '/webmozart/assert/src',
),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
), ),
'Recurr\\' => 'Recurr\\' =>
array ( array (
@ -153,8 +131,8 @@ class ComposerStaticInit20d4022bc196691807f55d4a47c06474
), ),
'Psr\\Http\\Message\\' => 'Psr\\Http\\Message\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/http-message/src', 0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-factory/src', 1 => __DIR__ . '/..' . '/psr/http-message/src',
), ),
'GuzzleHttp\\Psr7\\' => 'GuzzleHttp\\Psr7\\' =>
array ( array (
@ -170,7 +148,7 @@ class ComposerStaticInit20d4022bc196691807f55d4a47c06474
), ),
'Doctrine\\Common\\Collections\\' => 'Doctrine\\Common\\Collections\\' =>
array ( array (
0 => __DIR__ . '/..' . '/doctrine/collections/lib/Doctrine/Common/Collections', 0 => __DIR__ . '/..' . '/doctrine/collections/src',
), ),
'Cron\\' => 'Cron\\' =>
array ( array (
@ -200,22 +178,19 @@ class ComposerStaticInit20d4022bc196691807f55d4a47c06474
); );
public static $classMap = array ( public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'Deprecated' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'ReflectionConstant' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
'lessc' => __DIR__ . '/..' . '/wikimedia/less.php/lessc.inc.php', 'lessc' => __DIR__ . '/..' . '/wikimedia/less.php/lessc.inc.php',
); );
public static function getInitializer(ClassLoader $loader) public static function getInitializer(ClassLoader $loader)
{ {
return \Closure::bind(function () use ($loader) { return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit20d4022bc196691807f55d4a47c06474::$prefixLengthsPsr4; $loader->prefixLengthsPsr4 = ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit20d4022bc196691807f55d4a47c06474::$prefixDirsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit20d4022bc196691807f55d4a47c06474::$prefixesPsr0; $loader->prefixesPsr0 = ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa::$prefixesPsr0;
$loader->classMap = ComposerStaticInit20d4022bc196691807f55d4a47c06474::$classMap; $loader->classMap = ComposerStaticInit7a1692c86b6fc70eaaf43c4bee3673aa::$classMap;
}, null, ClassLoader::class); }, null, ClassLoader::class);
} }

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
'name' => 'icinga/icinga-php-library', 'name' => 'icinga/icinga-php-library',
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => 'a7944e40e1b1c5f88dcb04c253e07001cb9ddb4b', 'reference' => 'e15e7dee90f02adb3ae82280bfc3a803b09f12f5',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -11,27 +11,18 @@
), ),
'versions' => array( 'versions' => array(
'brick/math' => array( 'brick/math' => array(
'pretty_version' => '0.9.3', 'pretty_version' => '0.14.0',
'version' => '0.9.3.0', 'version' => '0.14.0.0',
'reference' => 'ca57d18f028f84f777b2168cd1911b0dee2343ae', 'reference' => '113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../brick/math', 'install_path' => __DIR__ . '/../brick/math',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'cweagans/composer-patches' => array(
'pretty_version' => '1.7.3',
'version' => '1.7.3.0',
'reference' => 'e190d4466fe2b103a55467dfa83fc2fecfcaf2db',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../cweagans/composer-patches',
'aliases' => array(),
'dev_requirement' => false,
),
'doctrine/collections' => array( 'doctrine/collections' => array(
'pretty_version' => '1.8.0', 'pretty_version' => '2.4.0',
'version' => '1.8.0.0', 'version' => '2.4.0.0',
'reference' => '2b44dd4cbca8b5744327de78bafef5945c7e7b5e', 'reference' => '9acfeea2e8666536edff3d77c531261c63680160',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/collections', 'install_path' => __DIR__ . '/../doctrine/collections',
'aliases' => array(), 'aliases' => array(),
@ -47,9 +38,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'dragonmantank/cron-expression' => array( 'dragonmantank/cron-expression' => array(
'pretty_version' => 'v3.4.0', 'pretty_version' => 'v3.6.0',
'version' => '3.4.0.0', 'version' => '3.6.0.0',
'reference' => '8c784d071debd117328803d86b2097615b457500', 'reference' => 'd61a8a9604ec1f8c3d150d09db6ce98b32675013',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../dragonmantank/cron-expression', 'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
'aliases' => array(), 'aliases' => array(),
@ -85,82 +76,106 @@
'icinga/icinga-php-library' => array( 'icinga/icinga-php-library' => array(
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => 'a7944e40e1b1c5f88dcb04c253e07001cb9ddb4b', 'reference' => 'e15e7dee90f02adb3ae82280bfc3a803b09f12f5',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/html' => array( 'ipl/html' => array(
'pretty_version' => 'v0.8.2', 'pretty_version' => 'dev-main',
'version' => '0.8.2.0', 'version' => 'dev-main',
'reference' => 'e18bdf11abca5e477100e2c7d190ef5f424d0d98', 'reference' => '9354c29faf807e67c4a0a365b450b7f0b64598ad',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/html', 'install_path' => __DIR__ . '/../ipl/html',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/i18n' => array( 'ipl/i18n' => array(
'pretty_version' => 'v0.2.2', 'pretty_version' => 'dev-main',
'version' => '0.2.2.0', 'version' => 'dev-main',
'reference' => 'a2b6109c5a93f86ce46d5dc351dbe75e8502cf8c', 'reference' => '692c33cf46fb8a4511da613dbf97c6216c345cc5',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/i18n', 'install_path' => __DIR__ . '/../ipl/i18n',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/orm' => array( 'ipl/orm' => array(
'pretty_version' => 'v0.6.3', 'pretty_version' => 'dev-main',
'version' => '0.6.3.0', 'version' => 'dev-main',
'reference' => 'a775a2745764a8dc7f28618cce69dcd7bbfd7915', 'reference' => 'dbcb2da01c168a02fe264e075e1c02c7233851df',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/orm', 'install_path' => __DIR__ . '/../ipl/orm',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/scheduler' => array( 'ipl/scheduler' => array(
'pretty_version' => 'v0.1.2', 'pretty_version' => 'dev-main',
'version' => '0.1.2.0', 'version' => 'dev-main',
'reference' => '6119afdea07b1390bd728e350e0d80b26ec8d6ba', 'reference' => 'd15143b2db2623b42bd25a013a73800dae2c6459',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/scheduler', 'install_path' => __DIR__ . '/../ipl/scheduler',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/sql' => array( 'ipl/sql' => array(
'pretty_version' => 'v0.7.1', 'pretty_version' => 'dev-main',
'version' => '0.7.1.0', 'version' => 'dev-main',
'reference' => 'e80f1b712c4b96099b0bf9096e6efe317a165e3b', 'reference' => '6f4258c4e3b20655db57d248e26edf7b54c04729',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/sql', 'install_path' => __DIR__ . '/../ipl/sql',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/stdlib' => array( 'ipl/stdlib' => array(
'pretty_version' => 'v0.14.0', 'pretty_version' => 'dev-main',
'version' => '0.14.0.0', 'version' => 'dev-main',
'reference' => 'bf5fc8f40b86bd90337db6f3be389be2a93fa64a', 'reference' => '9b7a903fbfc341da59f242149ac333594e4a6fa3',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/stdlib', 'install_path' => __DIR__ . '/../ipl/stdlib',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/validator' => array( 'ipl/validator' => array(
'pretty_version' => 'v0.5.0', 'pretty_version' => 'dev-main',
'version' => '0.5.0.0', 'version' => 'dev-main',
'reference' => 'a601fae0ed330e63cea50e4a2a6659ca1ad97bde', 'reference' => 'eac5c6c114d8007db5c24ae159fe6f55e89a946b',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/validator', 'install_path' => __DIR__ . '/../ipl/validator',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ipl/web' => array( 'ipl/web' => array(
'pretty_version' => 'v0.10.2', 'pretty_version' => 'dev-main',
'version' => '0.10.2.0', 'version' => 'dev-main',
'reference' => 'a3d134c0d67aa51a9b186519c76e718603fda835', 'reference' => '3e8867cbdde8af9724f64b84830503f8fcbd90e8',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ipl/web', 'install_path' => __DIR__ . '/../ipl/web',
'aliases' => array(), 'aliases' => array(
0 => '99.x-dev',
1 => '9999999-dev',
),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'mtdowling/cron-expression' => array( 'mtdowling/cron-expression' => array(
@ -218,18 +233,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ramsey/collection' => array( 'ramsey/collection' => array(
'pretty_version' => '1.1.4', 'pretty_version' => '2.1.1',
'version' => '1.1.4.0', 'version' => '2.1.1.0',
'reference' => 'ab2237657ad99667a5143e32ba2683c8029563d4', 'reference' => '344572933ad0181accbf4ba763e85a0306a8c5e2',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ramsey/collection', 'install_path' => __DIR__ . '/../ramsey/collection',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ramsey/uuid' => array( 'ramsey/uuid' => array(
'pretty_version' => '4.2.3', 'pretty_version' => '4.9.1',
'version' => '4.2.3.0', 'version' => '4.9.1.0',
'reference' => 'fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df', 'reference' => '81f941f6f729b1e3ceea61d9d014f8b6c6800440',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ramsey/uuid', 'install_path' => __DIR__ . '/../ramsey/uuid',
'aliases' => array(), 'aliases' => array(),
@ -256,7 +271,7 @@
'rhumsaa/uuid' => array( 'rhumsaa/uuid' => array(
'dev_requirement' => false, 'dev_requirement' => false,
'replaced' => array( 'replaced' => array(
0 => '4.2.3', 0 => '4.9.1',
), ),
), ),
'simshaun/recurr' => array( 'simshaun/recurr' => array(
@ -268,30 +283,12 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-ctype' => array( 'symfony/polyfill-php84' => array(
'pretty_version' => 'v1.33.0', 'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0', 'version' => '1.33.0.0',
'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638', 'reference' => 'd8ced4d875142b6a7426000426b8abc631d6b191',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype', 'install_path' => __DIR__ . '/../symfony/polyfill-php84',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
'webmozart/assert' => array(
'pretty_version' => '1.11.0',
'version' => '1.11.0.0',
'reference' => '11cb2199493b2f8a3b53e7f19068fc6aac760991',
'type' => 'library',
'install_path' => __DIR__ . '/../webmozart/assert',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),

View File

@ -4,8 +4,8 @@
$issues = array(); $issues = array();
if (!(PHP_VERSION_ID >= 70209)) { if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.9". You are running ' . PHP_VERSION . '.'; $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
} }
if ($issues) { if ($issues) {
@ -19,8 +19,7 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
} }
} }
trigger_error( throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues), 'Composer detected issues in your platform: ' . implode(' ', $issues)
E_USER_ERROR
); );
} }

View File

@ -1,30 +0,0 @@
{
"name": "cweagans/composer-patches",
"description": "Provides a way to patch Composer packages.",
"minimum-stability": "dev",
"license": "BSD-3-Clause",
"type": "composer-plugin",
"extra": {
"class": "cweagans\\Composer\\Patches"
},
"authors": [
{
"name": "Cameron Eagans",
"email": "me@cweagans.net"
}
],
"require": {
"php": ">=5.3.0",
"composer-plugin-api": "^1.0 || ^2.0"
},
"require-dev": {
"composer/composer": "~1.0 || ~2.0",
"phpunit/phpunit": "~4.6"
},
"autoload": {
"psr-4": {"cweagans\\Composer\\": "src"}
},
"autoload-dev": {
"psr-4": {"cweagans\\Composer\\Tests\\": "tests"}
}
}

View File

@ -1,70 +0,0 @@
<?php
/**
* @file
* Dispatch events when patches are applied.
*/
namespace cweagans\Composer;
use Composer\EventDispatcher\Event;
use Composer\Package\PackageInterface;
class PatchEvent extends Event {
/**
* @var PackageInterface $package
*/
protected $package;
/**
* @var string $url
*/
protected $url;
/**
* @var string $description
*/
protected $description;
/**
* Constructs a PatchEvent object.
*
* @param string $eventName
* @param PackageInterface $package
* @param string $url
* @param string $description
*/
public function __construct($eventName, PackageInterface $package, $url, $description) {
parent::__construct($eventName);
$this->package = $package;
$this->url = $url;
$this->description = $description;
}
/**
* Returns the package that is patched.
*
* @return PackageInterface
*/
public function getPackage() {
return $this->package;
}
/**
* Returns the url of the patch.
*
* @return string
*/
public function getUrl() {
return $this->url;
}
/**
* Returns the description of the patch.
*
* @return string
*/
public function getDescription() {
return $this->description;
}
}

View File

@ -1,30 +0,0 @@
<?php
/**
* @file
* Dispatch events when patches are applied.
*/
namespace cweagans\Composer;
class PatchEvents {
/**
* The PRE_PATCH_APPLY event occurs before a patch is applied.
*
* The event listener method receives a cweagans\Composer\PatchEvent instance.
*
* @var string
*/
const PRE_PATCH_APPLY = 'pre-patch-apply';
/**
* The POST_PATCH_APPLY event occurs after a patch is applied.
*
* The event listener method receives a cweagans\Composer\PatchEvent instance.
*
* @var string
*/
const POST_PATCH_APPLY = 'post-patch-apply';
}

View File

@ -1,599 +0,0 @@
<?php
/**
* @file
* Provides a way to patch Composer packages after installation.
*/
namespace cweagans\Composer;
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\PackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Installer\PackageEvents;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Installer\PackageEvent;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Symfony\Component\Process\Process;
class Patches implements PluginInterface, EventSubscriberInterface {
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var IOInterface $io
*/
protected $io;
/**
* @var EventDispatcher $eventDispatcher
*/
protected $eventDispatcher;
/**
* @var ProcessExecutor $executor
*/
protected $executor;
/**
* @var array $patches
*/
protected $patches;
/**
* @var array $installedPatches
*/
protected $installedPatches;
/**
* Apply plugin modifications to composer
*
* @param Composer $composer
* @param IOInterface $io
*/
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->eventDispatcher = $composer->getEventDispatcher();
$this->executor = new ProcessExecutor($this->io);
$this->patches = array();
$this->installedPatches = array();
}
/**
* Returns an array of event names this subscriber wants to listen to.
*/
public static function getSubscribedEvents() {
return array(
ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'),
ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'),
PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'),
PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'),
// The following is a higher weight for compatibility with
// https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with
// every Composer plugin which deploys downloaded packages to other locations.
// In such cases you want that those plugins deploy patched files so they have to run after
// the "composer-patches" plugin.
// @see: https://github.com/cweagans/composer-patches/pull/153
PackageEvents::POST_PACKAGE_INSTALL => array('postInstall', 10),
PackageEvents::POST_PACKAGE_UPDATE => array('postInstall', 10),
);
}
/**
* Before running composer install,
* @param Event $event
*/
public function checkPatches(Event $event) {
if (!$this->isPatchingEnabled()) {
return;
}
try {
$repositoryManager = $this->composer->getRepositoryManager();
$localRepository = $repositoryManager->getLocalRepository();
$installationManager = $this->composer->getInstallationManager();
$packages = $localRepository->getPackages();
$extra = $this->composer->getPackage()->getExtra();
$patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
$tmp_patches = $this->grabPatches();
foreach ($packages as $package) {
$extra = $package->getExtra();
if (isset($extra['patches'])) {
if (isset($patches_ignore[$package->getName()])) {
foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
if (isset($extra['patches'][$package_name])) {
$extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
}
}
}
$this->installedPatches[$package->getName()] = $extra['patches'];
}
$patches = isset($extra['patches']) ? $extra['patches'] : array();
$tmp_patches = $this->arrayMergeRecursiveDistinct($tmp_patches, $patches);
}
if ($tmp_patches == FALSE) {
$this->io->write('<info>No patches supplied.</info>');
return;
}
// Remove packages for which the patch set has changed.
$promises = array();
foreach ($packages as $package) {
if (!($package instanceof AliasPackage)) {
$package_name = $package->getName();
$extra = $package->getExtra();
$has_patches = isset($tmp_patches[$package_name]);
$has_applied_patches = isset($extra['patches_applied']) && count($extra['patches_applied']) > 0;
if (($has_patches && !$has_applied_patches)
|| (!$has_patches && $has_applied_patches)
|| ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) {
$uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.');
$this->io->write('<info>Removing package ' . $package_name . ' so that it can be re-installed and re-patched.</info>');
$promises[] = $installationManager->uninstall($localRepository, $uninstallOperation);
}
}
}
$promises = array_filter($promises);
if ($promises) {
$this->composer->getLoop()->wait($promises);
}
}
// If the Locker isn't available, then we don't need to do this.
// It's the first time packages have been installed.
catch (\LogicException $e) {
return;
}
}
/**
* Gather patches from dependencies and store them for later use.
*
* @param PackageEvent $event
*/
public function gatherPatches(PackageEvent $event) {
// If we've already done this, then don't do it again.
if (isset($this->patches['_patchesGathered'])) {
$this->io->write('<info>Patches already gathered. Skipping</info>', TRUE, IOInterface::VERBOSE);
return;
}
// If patching has been disabled, bail out here.
elseif (!$this->isPatchingEnabled()) {
$this->io->write('<info>Patching is disabled. Skipping.</info>', TRUE, IOInterface::VERBOSE);
return;
}
$this->patches = $this->grabPatches();
if (empty($this->patches)) {
$this->io->write('<info>No patches supplied.</info>');
}
$extra = $this->composer->getPackage()->getExtra();
$patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
// Now add all the patches from dependencies that will be installed.
$operations = $event->getOperations();
$this->io->write('<info>Gathering patches for dependencies. This might take a minute.</info>');
foreach ($operations as $operation) {
if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) {
$package = $this->getPackageFromOperation($operation);
$extra = $package->getExtra();
if (isset($extra['patches'])) {
if (isset($patches_ignore[$package->getName()])) {
foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
if (isset($extra['patches'][$package_name])) {
$extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
}
}
}
$this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']);
}
// Unset installed patches for this package
if(isset($this->installedPatches[$package->getName()])) {
unset($this->installedPatches[$package->getName()]);
}
}
}
// Merge installed patches from dependencies that did not receive an update.
foreach ($this->installedPatches as $patches) {
$this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $patches);
}
// If we're in verbose mode, list the projects we're going to patch.
if ($this->io->isVerbose()) {
foreach ($this->patches as $package => $patches) {
$number = count($patches);
$this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>');
}
}
// Make sure we don't gather patches again. Extra keys in $this->patches
// won't hurt anything, so we'll just stash it there.
$this->patches['_patchesGathered'] = TRUE;
}
/**
* Get the patches from root composer or external file
* @return Patches
* @throws \Exception
*/
public function grabPatches() {
// First, try to get the patches from the root composer.json.
$extra = $this->composer->getPackage()->getExtra();
if (isset($extra['patches'])) {
$this->io->write('<info>Gathering patches for root package.</info>');
$patches = $extra['patches'];
return $patches;
}
// If it's not specified there, look for a patches-file definition.
elseif (isset($extra['patches-file'])) {
$this->io->write('<info>Gathering patches from patch file.</info>');
$patches = file_get_contents($extra['patches-file']);
$patches = json_decode($patches, TRUE);
$error = json_last_error();
if ($error != 0) {
switch ($error) {
case JSON_ERROR_DEPTH:
$msg = ' - Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = ' - Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = ' - Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$msg = ' - Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$msg = ' - Unknown error';
break;
}
throw new \Exception('There was an error in the supplied patches file:' . $msg);
}
if (isset($patches['patches'])) {
$patches = $patches['patches'];
return $patches;
}
elseif(!$patches) {
throw new \Exception('There was an error in the supplied patch file');
}
}
else {
return array();
}
}
/**
* @param PackageEvent $event
* @throws \Exception
*/
public function postInstall(PackageEvent $event) {
// Check if we should exit in failure.
$extra = $this->composer->getPackage()->getExtra();
$exitOnFailure = getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure']);
$skipReporting = getenv('COMPOSER_PATCHES_SKIP_REPORTING') || !empty($extra['composer-patches-skip-reporting']);
// Get the package object for the current operation.
$operation = $event->getOperation();
/** @var PackageInterface $package */
$package = $this->getPackageFromOperation($operation);
$package_name = $package->getName();
if (!isset($this->patches[$package_name])) {
if ($this->io->isVerbose()) {
$this->io->write('<info>No patches found for ' . $package_name . '.</info>');
}
return;
}
$this->io->write(' - Applying patches for <info>' . $package_name . '</info>');
// Get the install path from the package object.
$manager = $event->getComposer()->getInstallationManager();
$install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
// Set up a downloader.
$downloader = new RemoteFilesystem($this->io, $this->composer->getConfig());
// Track applied patches in the package info in installed.json
$localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
$localPackage = $localRepository->findPackage($package_name, $package->getVersion());
$extra = $localPackage->getExtra();
$extra['patches_applied'] = array();
foreach ($this->patches[$package_name] as $description => $url) {
$this->io->write(' <info>' . $url . '</info> (<comment>' . $description. '</comment>)');
try {
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
$this->getAndApplyPatch($downloader, $install_path, $url, $package);
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description));
$extra['patches_applied'][$description] = $url;
}
catch (\Exception $e) {
$this->io->write(' <error>Could not apply patch! Skipping. The error was: ' . $e->getMessage() . '</error>');
if ($exitOnFailure) {
throw new \Exception("Cannot apply patch $description ($url)!");
}
}
}
$localPackage->setExtra($extra);
$this->io->write('');
if (true !== $skipReporting) {
$this->writePatchReport($this->patches[$package_name], $install_path);
}
}
/**
* Get a Package object from an OperationInterface object.
*
* @param OperationInterface $operation
* @return PackageInterface
* @throws \Exception
*/
protected function getPackageFromOperation(OperationInterface $operation) {
if ($operation instanceof InstallOperation) {
$package = $operation->getPackage();
}
elseif ($operation instanceof UpdateOperation) {
$package = $operation->getTargetPackage();
}
else {
throw new \Exception('Unknown operation: ' . get_class($operation));
}
return $package;
}
/**
* Apply a patch on code in the specified directory.
*
* @param RemoteFilesystem $downloader
* @param $install_path
* @param $patch_url
* @param PackageInterface $package
* @throws \Exception
*/
protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url, PackageInterface $package) {
// Local patch file.
if (file_exists($patch_url)) {
$filename = realpath($patch_url);
}
else {
// Generate random (but not cryptographically so) filename.
$filename = uniqid(sys_get_temp_dir().'/') . ".patch";
// Download file from remote filesystem to this location.
$hostname = parse_url($patch_url, PHP_URL_HOST);
try {
$downloader->copy($hostname, $patch_url, $filename, false);
} catch (\Exception $e) {
// In case of an exception, retry once as the download might
// have failed due to intermittent network issues.
$downloader->copy($hostname, $patch_url, $filename, false);
}
}
// The order here is intentional. p1 is most likely to apply with git apply.
// p0 is next likely. p2 is extremely unlikely, but for some special cases,
// it might be useful. p4 is useful for Magento 2 patches
$patch_levels = array('-p1', '-p0', '-p2', '-p4');
// Check for specified patch level for this package.
$extra = $this->composer->getPackage()->getExtra();
if (!empty($extra['patchLevel'][$package->getName()])){
$patch_levels = array($extra['patchLevel'][$package->getName()]);
}
// Attempt to apply with git apply.
$patched = $this->applyPatchWithGit($install_path, $patch_levels, $filename);
// In some rare cases, git will fail to apply a patch, fallback to using
// the 'patch' command.
if (!$patched) {
foreach ($patch_levels as $patch_level) {
// --no-backup-if-mismatch here is a hack that fixes some
// differences between how patch works on windows and unix.
if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) {
break;
}
}
}
// Clean up the temporary patch file.
if (isset($hostname)) {
unlink($filename);
}
// If the patch *still* isn't applied, then give up and throw an Exception.
// Otherwise, let the user know it worked.
if (!$patched) {
throw new \Exception("Cannot apply patch $patch_url");
}
}
/**
* Checks if the root package enables patching.
*
* @return bool
* Whether patching is enabled. Defaults to TRUE.
*/
protected function isPatchingEnabled() {
$extra = $this->composer->getPackage()->getExtra();
if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) {
// The root package has no patches of its own, so only allow patching if
// it has specifically opted in.
return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE;
}
else {
return TRUE;
}
}
/**
* Writes a patch report to the target directory.
*
* @param array $patches
* @param string $directory
*/
protected function writePatchReport($patches, $directory) {
$output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n";
$output .= "Patches applied to this directory:\n\n";
foreach ($patches as $description => $url) {
$output .= $description . "\n";
$output .= 'Source: ' . $url . "\n\n\n";
}
file_put_contents($directory . "/PATCHES.txt", $output);
}
/**
* Executes a shell command with escaping.
*
* @param string $cmd
* @return bool
*/
protected function executeCommand($cmd) {
// Shell-escape all arguments except the command.
$args = func_get_args();
foreach ($args as $index => $arg) {
if ($index !== 0) {
$args[$index] = escapeshellarg($arg);
}
}
// And replace the arguments.
$command = call_user_func_array('sprintf', $args);
$output = '';
if ($this->io->isVerbose()) {
$this->io->write('<comment>' . $command . '</comment>');
$io = $this->io;
$output = function ($type, $data) use ($io) {
if ($type == Process::ERR) {
$io->write('<error>' . $data . '</error>');
}
else {
$io->write('<comment>' . $data . '</comment>');
}
};
}
return ($this->executor->execute($command, $output) == 0);
}
/**
* Recursively merge arrays without changing data types of values.
*
* Does not change the data types of the values in the arrays. Matching keys'
* values in the second array overwrite those in the first array, as is the
* case with array_merge.
*
* @param array $array1
* The first array.
* @param array $array2
* The second array.
* @return array
* The merged array.
*
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195
*/
protected function arrayMergeRecursiveDistinct(array $array1, array $array2) {
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
}
else {
$merged[$key] = $value;
}
}
return $merged;
}
/**
* Attempts to apply a patch with git apply.
*
* @param $install_path
* @param $patch_levels
* @param $filename
*
* @return bool
* TRUE if patch was applied, FALSE otherwise.
*/
protected function applyPatchWithGit($install_path, $patch_levels, $filename) {
// Do not use git apply unless the install path is itself a git repo
// @see https://stackoverflow.com/a/27283285
if (!is_dir($install_path . '/.git')) {
return FALSE;
}
$patched = FALSE;
foreach ($patch_levels as $patch_level) {
if ($this->io->isVerbose()) {
$comment = 'Testing ability to patch with git apply.';
$comment .= ' This command may produce errors that can be safely ignored.';
$this->io->write('<comment>' . $comment . '</comment>');
}
$checked = $this->executeCommand('git -C %s apply --check -v %s %s', $install_path, $patch_level, $filename);
$output = $this->executor->getErrorOutput();
if (substr($output, 0, 7) == 'Skipped') {
// Git will indicate success but silently skip patches in some scenarios.
//
// @see https://github.com/cweagans/composer-patches/pull/165
$checked = FALSE;
}
if ($checked) {
// Apply the first successful style.
$patched = $this->executeCommand('git -C %s apply %s %s', $install_path, $patch_level, $filename);
break;
}
}
return $patched;
}
/**
* Indicates if a package has been patched.
*
* @param \Composer\Package\PackageInterface $package
* The package to check.
*
* @return bool
* TRUE if the package has been patched.
*/
public static function isPackagePatched(PackageInterface $package) {
return array_key_exists('patches_applied', $package->getExtra());
}
/**
* {@inheritDoc}
*/
public function deactivate(Composer $composer, IOInterface $io)
{
}
/**
* {@inheritDoc}
*/
public function uninstall(Composer $composer, IOInterface $io)
{
}
}

View File

@ -1,32 +0,0 @@
{
"active": true,
"name": "Collections",
"slug": "collections",
"docsSlug": "doctrine-collections",
"versions": [
{
"name": "2.0",
"branchName": "2.0.x",
"slug": "latest",
"upcoming": true
},
{
"name": "1.8",
"branchName": "1.8.x",
"slug": "1.8",
"upcoming": true
},
{
"name": "1.7",
"branchName": "1.7.x",
"slug": "1.7",
"current": true
},
{
"name": "1.6",
"branchName": "1.6.x",
"slug": "1.6",
"maintained": false
}
]
}

View File

@ -33,23 +33,25 @@
], ],
"homepage": "https://www.doctrine-project.org/projects/collections.html", "homepage": "https://www.doctrine-project.org/projects/collections.html",
"require": { "require": {
"php": "^7.1.3 || ^8.0", "php": "^8.1",
"doctrine/deprecations": "^0.5.3 || ^1" "doctrine/deprecations": "^1",
"symfony/polyfill-php84": "^1.30"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^9.0 || ^10.0", "ext-json": "*",
"phpstan/phpstan": "^1.4.8", "doctrine/coding-standard": "^14",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", "phpstan/phpstan": "^2.1.30",
"vimeo/psalm": "^4.22" "phpstan/phpstan-phpunit": "^2.0.7",
"phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" "Doctrine\\Common\\Collections\\": "src"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Doctrine\\Tests\\": "tests/Doctrine/Tests" "Doctrine\\Tests\\Common\\Collections\\": "tests"
} }
}, },
"config": { "config": {

View File

@ -0,0 +1,5 @@
{
"require": {
"doctrine/docs-builder": "^1.0"
}
}

View File

@ -1,269 +0,0 @@
<?php
namespace Doctrine\Common\Collections\Expr;
use ArrayAccess;
use Closure;
use RuntimeException;
use function explode;
use function in_array;
use function is_array;
use function is_scalar;
use function iterator_to_array;
use function method_exists;
use function preg_match;
use function preg_replace_callback;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;
/**
* Walks an expression graph and turns it into a PHP closure.
*
* This closure can be used with {@Collection#filter()} and is used internally
* by {@ArrayCollection#select()}.
*/
class ClosureExpressionVisitor extends ExpressionVisitor
{
/**
* Accesses the field of a given object. This field has to be public
* directly or indirectly (through an accessor get*, is*, or a magic
* method, __get, __call).
*
* @param object|mixed[] $object
* @param string $field
*
* @return mixed
*/
public static function getObjectFieldValue($object, $field)
{
if (strpos($field, '.') !== false) {
[$field, $subField] = explode('.', $field, 2);
$object = self::getObjectFieldValue($object, $field);
return self::getObjectFieldValue($object, $subField);
}
if (is_array($object)) {
return $object[$field];
}
$accessors = ['get', 'is', ''];
foreach ($accessors as $accessor) {
$accessor .= $field;
if (method_exists($object, $accessor)) {
return $object->$accessor();
}
}
if (preg_match('/^is[A-Z]+/', $field) === 1 && method_exists($object, $field)) {
return $object->$field();
}
// __call should be triggered for get.
$accessor = $accessors[0] . $field;
if (method_exists($object, '__call')) {
return $object->$accessor();
}
if ($object instanceof ArrayAccess) {
return $object[$field];
}
if (isset($object->$field)) {
return $object->$field;
}
// camelcase field name to support different variable naming conventions
$ccField = preg_replace_callback('/_(.?)/', static function ($matches) {
return strtoupper($matches[1]);
}, $field);
foreach ($accessors as $accessor) {
$accessor .= $ccField;
if (method_exists($object, $accessor)) {
return $object->$accessor();
}
}
return $object->$field;
}
/**
* Helper for sorting arrays of objects based on multiple fields + orientations.
*
* @param string $name
* @param int $orientation
*
* @return Closure
*/
public static function sortByField($name, $orientation = 1, ?Closure $next = null)
{
if (! $next) {
$next = static function (): int {
return 0;
};
}
return static function ($a, $b) use ($name, $next, $orientation): int {
$aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name);
$bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name);
if ($aValue === $bValue) {
return $next($a, $b);
}
return ($aValue > $bValue ? 1 : -1) * $orientation;
};
}
/**
* {@inheritDoc}
*/
public function walkComparison(Comparison $comparison)
{
$field = $comparison->getField();
$value = $comparison->getValue()->getValue(); // shortcut for walkValue()
switch ($comparison->getOperator()) {
case Comparison::EQ:
return static function ($object) use ($field, $value): bool {
return ClosureExpressionVisitor::getObjectFieldValue($object, $field) === $value;
};
case Comparison::NEQ:
return static function ($object) use ($field, $value): bool {
return ClosureExpressionVisitor::getObjectFieldValue($object, $field) !== $value;
};
case Comparison::LT:
return static function ($object) use ($field, $value): bool {
return ClosureExpressionVisitor::getObjectFieldValue($object, $field) < $value;
};
case Comparison::LTE:
return static function ($object) use ($field, $value): bool {
return ClosureExpressionVisitor::getObjectFieldValue($object, $field) <= $value;
};
case Comparison::GT:
return static function ($object) use ($field, $value): bool {
return ClosureExpressionVisitor::getObjectFieldValue($object, $field) > $value;
};
case Comparison::GTE:
return static function ($object) use ($field, $value): bool {
return ClosureExpressionVisitor::getObjectFieldValue($object, $field) >= $value;
};
case Comparison::IN:
return static function ($object) use ($field, $value): bool {
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
return in_array($fieldValue, $value, is_scalar($fieldValue));
};
case Comparison::NIN:
return static function ($object) use ($field, $value): bool {
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
return ! in_array($fieldValue, $value, is_scalar($fieldValue));
};
case Comparison::CONTAINS:
return static function ($object) use ($field, $value) {
return strpos(ClosureExpressionVisitor::getObjectFieldValue($object, $field), $value) !== false;
};
case Comparison::MEMBER_OF:
return static function ($object) use ($field, $value): bool {
$fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
if (! is_array($fieldValues)) {
$fieldValues = iterator_to_array($fieldValues);
}
return in_array($value, $fieldValues, true);
};
case Comparison::STARTS_WITH:
return static function ($object) use ($field, $value): bool {
return strpos(ClosureExpressionVisitor::getObjectFieldValue($object, $field), $value) === 0;
};
case Comparison::ENDS_WITH:
return static function ($object) use ($field, $value): bool {
return $value === substr(ClosureExpressionVisitor::getObjectFieldValue($object, $field), -strlen($value));
};
default:
throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator());
}
}
/**
* {@inheritDoc}
*/
public function walkValue(Value $value)
{
return $value->getValue();
}
/**
* {@inheritDoc}
*/
public function walkCompositeExpression(CompositeExpression $expr)
{
$expressionList = [];
foreach ($expr->getExpressionList() as $child) {
$expressionList[] = $this->dispatch($child);
}
switch ($expr->getType()) {
case CompositeExpression::TYPE_AND:
return $this->andExpressions($expressionList);
case CompositeExpression::TYPE_OR:
return $this->orExpressions($expressionList);
default:
throw new RuntimeException('Unknown composite ' . $expr->getType());
}
}
/** @param callable[] $expressions */
private function andExpressions(array $expressions): callable
{
return static function ($object) use ($expressions): bool {
foreach ($expressions as $expression) {
if (! $expression($object)) {
return false;
}
}
return true;
};
}
/** @param callable[] $expressions */
private function orExpressions(array $expressions): callable
{
return static function ($object) use ($expressions): bool {
foreach ($expressions as $expression) {
if ($expression($object)) {
return true;
}
}
return false;
};
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace Doctrine\Common\Collections\Expr;
/**
* Comparison of a field with a value by the given operator.
*/
class Comparison implements Expression
{
public const EQ = '=';
public const NEQ = '<>';
public const LT = '<';
public const LTE = '<=';
public const GT = '>';
public const GTE = '>=';
public const IS = '='; // no difference with EQ
public const IN = 'IN';
public const NIN = 'NIN';
public const CONTAINS = 'CONTAINS';
public const MEMBER_OF = 'MEMBER_OF';
public const STARTS_WITH = 'STARTS_WITH';
public const ENDS_WITH = 'ENDS_WITH';
/** @var string */
private $field;
/** @var string */
private $op;
/** @var Value */
private $value;
/**
* @param string $field
* @param string $operator
* @param mixed $value
*/
public function __construct($field, $operator, $value)
{
if (! ($value instanceof Value)) {
$value = new Value($value);
}
$this->field = $field;
$this->op = $operator;
$this->value = $value;
}
/** @return string */
public function getField()
{
return $this->field;
}
/** @return Value */
public function getValue()
{
return $this->value;
}
/** @return string */
public function getOperator()
{
return $this->op;
}
/**
* {@inheritDoc}
*/
public function visit(ExpressionVisitor $visitor)
{
return $visitor->walkComparison($this);
}
}

View File

@ -1,181 +0,0 @@
<?php
namespace Doctrine\Common\Collections;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Common\Collections\Expr\CompositeExpression;
use Doctrine\Common\Collections\Expr\Value;
use function func_get_args;
/**
* Builder for Expressions in the {@link Selectable} interface.
*
* Important Notice for interoperable code: You have to use scalar
* values only for comparisons, otherwise the behavior of the comparison
* may be different between implementations (Array vs ORM vs ODM).
*/
class ExpressionBuilder
{
/**
* @param mixed ...$x
*
* @return CompositeExpression
*/
public function andX($x = null)
{
return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args());
}
/**
* @param mixed ...$x
*
* @return CompositeExpression
*/
public function orX($x = null)
{
return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args());
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function eq($field, $value)
{
return new Comparison($field, Comparison::EQ, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function gt($field, $value)
{
return new Comparison($field, Comparison::GT, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function lt($field, $value)
{
return new Comparison($field, Comparison::LT, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function gte($field, $value)
{
return new Comparison($field, Comparison::GTE, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function lte($field, $value)
{
return new Comparison($field, Comparison::LTE, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function neq($field, $value)
{
return new Comparison($field, Comparison::NEQ, new Value($value));
}
/**
* @param string $field
*
* @return Comparison
*/
public function isNull($field)
{
return new Comparison($field, Comparison::EQ, new Value(null));
}
/**
* @param string $field
* @param mixed[] $values
*
* @return Comparison
*/
public function in($field, array $values)
{
return new Comparison($field, Comparison::IN, new Value($values));
}
/**
* @param string $field
* @param mixed[] $values
*
* @return Comparison
*/
public function notIn($field, array $values)
{
return new Comparison($field, Comparison::NIN, new Value($values));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function contains($field, $value)
{
return new Comparison($field, Comparison::CONTAINS, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function memberOf($field, $value)
{
return new Comparison($field, Comparison::MEMBER_OF, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function startsWith($field, $value)
{
return new Comparison($field, Comparison::STARTS_WITH, new Value($value));
}
/**
* @param string $field
* @param mixed $value
*
* @return Comparison
*/
public function endsWith($field, $value)
{
return new Comparison($field, Comparison::ENDS_WITH, new Value($value));
}
}

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections; namespace Doctrine\Common\Collections;
use Closure; use Closure;
@ -10,8 +12,8 @@ use Traversable;
/** /**
* Lazy collection that is backed by a concrete collection * Lazy collection that is backed by a concrete collection
* *
* @psalm-template TKey of array-key * @phpstan-template TKey of array-key
* @psalm-template T * @phpstan-template T
* @template-implements Collection<TKey,T> * @template-implements Collection<TKey,T>
*/ */
abstract class AbstractLazyCollection implements Collection abstract class AbstractLazyCollection implements Collection
@ -19,13 +21,12 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* The backed collection to use * The backed collection to use
* *
* @psalm-var Collection<TKey,T>|null * @phpstan-var Collection<TKey,T>|null
* @var Collection<mixed>|null * @var Collection<mixed>|null
*/ */
protected $collection; protected Collection|null $collection;
/** @var bool */ protected bool $initialized = false;
protected $initialized = false;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -43,11 +44,11 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function add($element) public function add(mixed $element)
{ {
$this->initialize(); $this->initialize();
return $this->collection->add($element); $this->collection->add($element);
} }
/** /**
@ -61,10 +62,8 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*
* @template TMaybeContained
*/ */
public function contains($element) public function contains(mixed $element)
{ {
$this->initialize(); $this->initialize();
@ -84,7 +83,7 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function remove($key) public function remove(string|int $key)
{ {
$this->initialize(); $this->initialize();
@ -94,7 +93,7 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function removeElement($element) public function removeElement(mixed $element)
{ {
$this->initialize(); $this->initialize();
@ -104,7 +103,7 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function containsKey($key) public function containsKey(string|int $key)
{ {
$this->initialize(); $this->initialize();
@ -114,7 +113,7 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function get($key) public function get(string|int $key)
{ {
$this->initialize(); $this->initialize();
@ -144,7 +143,7 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function set($key, $value) public function set(string|int $key, mixed $value)
{ {
$this->initialize(); $this->initialize();
$this->collection->set($key, $value); $this->collection->set($key, $value);
@ -220,6 +219,16 @@ abstract class AbstractLazyCollection implements Collection
return $this->collection->exists($p); return $this->collection->exists($p);
} }
/**
* {@inheritDoc}
*/
public function findFirst(Closure $p)
{
$this->initialize();
return $this->collection->findFirst($p);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -250,6 +259,16 @@ abstract class AbstractLazyCollection implements Collection
return $this->collection->map($func); return $this->collection->map($func);
} }
/**
* {@inheritDoc}
*/
public function reduce(Closure $func, mixed $initial = null)
{
$this->initialize();
return $this->collection->reduce($func, $initial);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -265,7 +284,7 @@ abstract class AbstractLazyCollection implements Collection
* *
* @template TMaybeContained * @template TMaybeContained
*/ */
public function indexOf($element) public function indexOf(mixed $element)
{ {
$this->initialize(); $this->initialize();
@ -275,7 +294,7 @@ abstract class AbstractLazyCollection implements Collection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function slice($offset, $length = null) public function slice(int $offset, int|null $length = null)
{ {
$this->initialize(); $this->initialize();
@ -286,7 +305,7 @@ abstract class AbstractLazyCollection implements Collection
* {@inheritDoc} * {@inheritDoc}
* *
* @return Traversable<int|string, mixed> * @return Traversable<int|string, mixed>
* @psalm-return Traversable<TKey,T> * @phpstan-return Traversable<TKey,T>
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function getIterator() public function getIterator()
@ -297,12 +316,14 @@ abstract class AbstractLazyCollection implements Collection
} }
/** /**
* {@inheritDoc}
*
* @param TKey $offset * @param TKey $offset
* *
* @return bool * @return bool
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetExists($offset) public function offsetExists(mixed $offset)
{ {
$this->initialize(); $this->initialize();
@ -310,12 +331,14 @@ abstract class AbstractLazyCollection implements Collection
} }
/** /**
* {@inheritDoc}
*
* @param TKey $offset * @param TKey $offset
* *
* @return mixed * @return T|null
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetGet($offset) public function offsetGet(mixed $offset)
{ {
$this->initialize(); $this->initialize();
@ -323,13 +346,15 @@ abstract class AbstractLazyCollection implements Collection
} }
/** /**
* {@inheritDoc}
*
* @param TKey|null $offset * @param TKey|null $offset
* @param T $value * @param T $value
* *
* @return void * @return void
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetSet($offset, $value) public function offsetSet(mixed $offset, mixed $value)
{ {
$this->initialize(); $this->initialize();
$this->collection->offsetSet($offset, $value); $this->collection->offsetSet($offset, $value);
@ -341,7 +366,7 @@ abstract class AbstractLazyCollection implements Collection
* @return void * @return void
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetUnset($offset) public function offsetUnset(mixed $offset)
{ {
$this->initialize(); $this->initialize();
$this->collection->offsetUnset($offset); $this->collection->offsetUnset($offset);
@ -352,7 +377,7 @@ abstract class AbstractLazyCollection implements Collection
* *
* @return bool * @return bool
* *
* @psalm-assert-if-true Collection<TKey,T> $this->collection * @phpstan-assert-if-true Collection<TKey,T> $this->collection
*/ */
public function isInitialized() public function isInitialized()
{ {
@ -364,7 +389,7 @@ abstract class AbstractLazyCollection implements Collection
* *
* @return void * @return void
* *
* @psalm-assert Collection<TKey,T> $this->collection * @phpstan-assert Collection<TKey,T> $this->collection
*/ */
protected function initialize() protected function initialize()
{ {

View File

@ -1,17 +1,24 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections; namespace Doctrine\Common\Collections;
use ArrayIterator; use ArrayIterator;
use Closure; use Closure;
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor; use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
use ReturnTypeWillChange; use ReturnTypeWillChange;
use Stringable;
use Traversable; use Traversable;
use function array_all;
use function array_any;
use function array_filter; use function array_filter;
use function array_find;
use function array_key_exists; use function array_key_exists;
use function array_keys; use function array_keys;
use function array_map; use function array_map;
use function array_reduce;
use function array_reverse; use function array_reverse;
use function array_search; use function array_search;
use function array_slice; use function array_slice;
@ -36,27 +43,26 @@ use const ARRAY_FILTER_USE_BOTH;
* serialize a collection use {@link toArray()} and reconstruct the collection * serialize a collection use {@link toArray()} and reconstruct the collection
* manually. * manually.
* *
* @psalm-template TKey of array-key * @phpstan-template TKey of array-key
* @psalm-template T * @phpstan-template T
* @template-implements Collection<TKey,T> * @template-implements Collection<TKey,T>
* @template-implements Selectable<TKey,T> * @template-implements Selectable<TKey,T>
* @psalm-consistent-constructor * @phpstan-consistent-constructor
*/ */
class ArrayCollection implements Collection, Selectable class ArrayCollection implements Collection, Selectable, Stringable
{ {
/** /**
* An array containing the entries of this collection. * An array containing the entries of this collection.
* *
* @psalm-var array<TKey,T> * @phpstan-var array<TKey,T>
* @var mixed[] * @var mixed[]
*/ */
private $elements; private array $elements = [];
/** /**
* Initializes a new ArrayCollection. * Initializes a new ArrayCollection.
* *
* @param array $elements * @phpstan-param array<TKey,T> $elements
* @psalm-param array<TKey,T> $elements
*/ */
public function __construct(array $elements = []) public function __construct(array $elements = [])
{ {
@ -86,13 +92,13 @@ class ArrayCollection implements Collection, Selectable
* instance should be created when constructor semantics have changed. * instance should be created when constructor semantics have changed.
* *
* @param array $elements Elements. * @param array $elements Elements.
* @psalm-param array<K,V> $elements * @phpstan-param array<K,V> $elements
* *
* @return static * @return static
* @psalm-return static<K,V> * @phpstan-return static<K,V>
* *
* @psalm-template K of array-key * @phpstan-template K of array-key
* @psalm-template V * @phpstan-template V
*/ */
protected function createFrom(array $elements) protected function createFrom(array $elements)
{ {
@ -134,7 +140,7 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function remove($key) public function remove(string|int $key)
{ {
if (! isset($this->elements[$key]) && ! array_key_exists($key, $this->elements)) { if (! isset($this->elements[$key]) && ! array_key_exists($key, $this->elements)) {
return null; return null;
@ -149,7 +155,7 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function removeElement($element) public function removeElement(mixed $element)
{ {
$key = array_search($element, $this->elements, true); $key = array_search($element, $this->elements, true);
@ -170,7 +176,7 @@ class ArrayCollection implements Collection, Selectable
* @return bool * @return bool
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetExists($offset) public function offsetExists(mixed $offset)
{ {
return $this->containsKey($offset); return $this->containsKey($offset);
} }
@ -180,10 +186,10 @@ class ArrayCollection implements Collection, Selectable
* *
* @param TKey $offset * @param TKey $offset
* *
* @return mixed * @return T|null
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetGet($offset) public function offsetGet(mixed $offset)
{ {
return $this->get($offset); return $this->get($offset);
} }
@ -197,9 +203,9 @@ class ArrayCollection implements Collection, Selectable
* @return void * @return void
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetSet($offset, $value) public function offsetSet(mixed $offset, mixed $value)
{ {
if (! isset($offset)) { if ($offset === null) {
$this->add($value); $this->add($value);
return; return;
@ -216,7 +222,7 @@ class ArrayCollection implements Collection, Selectable
* @return void * @return void
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function offsetUnset($offset) public function offsetUnset(mixed $offset)
{ {
$this->remove($offset); $this->remove($offset);
} }
@ -224,17 +230,15 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function containsKey($key) public function containsKey(string|int $key)
{ {
return isset($this->elements[$key]) || array_key_exists($key, $this->elements); return isset($this->elements[$key]) || array_key_exists($key, $this->elements);
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
*
* @template TMaybeContained
*/ */
public function contains($element) public function contains(mixed $element)
{ {
return in_array($element, $this->elements, true); return in_array($element, $this->elements, true);
} }
@ -244,21 +248,19 @@ class ArrayCollection implements Collection, Selectable
*/ */
public function exists(Closure $p) public function exists(Closure $p)
{ {
foreach ($this->elements as $key => $element) { return array_any(
if ($p($key, $element)) { $this->elements,
return true; static fn (mixed $element, mixed $key): bool => (bool) $p($key, $element),
} );
}
return false;
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* @psalm-param TMaybeContained $element * @phpstan-param TMaybeContained $element
* *
* @psalm-return (TMaybeContained is T ? TKey|false : false) * @return int|string|false
* @phpstan-return (TMaybeContained is T ? TKey|false : false)
* *
* @template TMaybeContained * @template TMaybeContained
*/ */
@ -270,7 +272,7 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function get($key) public function get(string|int $key)
{ {
return $this->elements[$key] ?? null; return $this->elements[$key] ?? null;
} }
@ -294,7 +296,7 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* @return int * @return int<0, max>
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function count() public function count()
@ -305,7 +307,7 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function set($key, $value) public function set(string|int $key, mixed $value)
{ {
$this->elements[$key] = $value; $this->elements[$key] = $value;
} }
@ -313,16 +315,12 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* @psalm-suppress InvalidPropertyAssignmentValue
*
* This breaks assumptions about the template type, but it would * This breaks assumptions about the template type, but it would
* be a backwards-incompatible change to remove this method * be a backwards-incompatible change to remove this method
*/ */
public function add($element) public function add(mixed $element)
{ {
$this->elements[] = $element; $this->elements[] = $element;
return true;
} }
/** /**
@ -337,7 +335,7 @@ class ArrayCollection implements Collection, Selectable
* {@inheritDoc} * {@inheritDoc}
* *
* @return Traversable<int|string, mixed> * @return Traversable<int|string, mixed>
* @psalm-return Traversable<TKey,T> * @phpstan-return Traversable<TKey, T>
*/ */
#[ReturnTypeWillChange] #[ReturnTypeWillChange]
public function getIterator() public function getIterator()
@ -348,12 +346,12 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* @psalm-param Closure(T):U $func * @phpstan-param Closure(T):U $func
* *
* @return static * @return static
* @psalm-return static<TKey, U> * @phpstan-return static<TKey, U>
* *
* @psalm-template U * @phpstan-template U
*/ */
public function map(Closure $func) public function map(Closure $func)
{ {
@ -362,27 +360,45 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/
public function reduce(Closure $func, $initial = null)
{
return array_reduce($this->elements, $func, $initial);
}
/**
* {@inheritDoc}
*
* @phpstan-param Closure(T, TKey):bool $p
* *
* @return static * @return static
* @psalm-return static<TKey,T> * @phpstan-return static<TKey,T>
*/ */
public function filter(Closure $p) public function filter(Closure $p)
{ {
return $this->createFrom(array_filter($this->elements, $p, ARRAY_FILTER_USE_BOTH)); return $this->createFrom(array_filter($this->elements, $p, ARRAY_FILTER_USE_BOTH));
} }
/**
* {@inheritDoc}
*/
public function findFirst(Closure $p)
{
return array_find(
$this->elements,
static fn (mixed $element, mixed $key): bool => (bool) $p($key, $element),
);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function forAll(Closure $p) public function forAll(Closure $p)
{ {
foreach ($this->elements as $key => $element) { return array_all(
if (! $p($key, $element)) { $this->elements,
return false; static fn (mixed $element, mixed $key): bool => (bool) $p($key, $element),
} );
}
return true;
} }
/** /**
@ -405,9 +421,11 @@ class ArrayCollection implements Collection, Selectable
/** /**
* Returns a string representation of this object. * Returns a string representation of this object.
* {@inheritDoc}
* *
* @return string * @return string
*/ */
#[ReturnTypeWillChange]
public function __toString() public function __toString()
{ {
return self::class . '@' . spl_object_hash($this); return self::class . '@' . spl_object_hash($this);
@ -424,31 +442,31 @@ class ArrayCollection implements Collection, Selectable
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function slice($offset, $length = null) public function slice(int $offset, int|null $length = null)
{ {
return array_slice($this->elements, $offset, $length, true); return array_slice($this->elements, $offset, $length, true);
} }
/** /** @phpstan-return Collection<TKey, T>&Selectable<TKey,T> */
* {@inheritDoc}
*/
public function matching(Criteria $criteria) public function matching(Criteria $criteria)
{ {
$accessRawFieldValues = $criteria->isRawFieldValueAccessEnabled();
$expr = $criteria->getWhereExpression(); $expr = $criteria->getWhereExpression();
$filtered = $this->elements; $filtered = $this->elements;
if ($expr) { if ($expr) {
$visitor = new ClosureExpressionVisitor(); $visitor = new ClosureExpressionVisitor($accessRawFieldValues);
$filter = $visitor->dispatch($expr); $filter = $visitor->dispatch($expr);
$filtered = array_filter($filtered, $filter); $filtered = array_filter($filtered, $filter);
} }
$orderings = $criteria->getOrderings(); $orderings = $criteria->orderings();
if ($orderings) { if ($orderings) {
$next = null; $next = null;
foreach (array_reverse($orderings) as $field => $ordering) { foreach (array_reverse($orderings) as $field => $ordering) {
$next = ClosureExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next); $next = ClosureExpressionVisitor::sortByField($field, $ordering === Order::Descending ? -1 : 1, $next, $accessRawFieldValues);
} }
uasort($filtered, $next); uasort($filtered, $next);
@ -457,8 +475,8 @@ class ArrayCollection implements Collection, Selectable
$offset = $criteria->getFirstResult(); $offset = $criteria->getFirstResult();
$length = $criteria->getMaxResults(); $length = $criteria->getMaxResults();
if ($offset || $length) { if ($offset !== null && $offset > 0 || $length !== null && $length > 0) {
$filtered = array_slice($filtered, (int) $offset, $length); $filtered = array_slice($filtered, (int) $offset, $length, true);
} }
return $this->createFrom($filtered); return $this->createFrom($filtered);

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections; namespace Doctrine\Common\Collections;
use ArrayAccess; use ArrayAccess;
@ -22,8 +24,8 @@ use Closure;
* position unless you explicitly positioned it before. Prefer iteration with * position unless you explicitly positioned it before. Prefer iteration with
* external iterators. * external iterators.
* *
* @psalm-template TKey of array-key * @phpstan-template TKey of array-key
* @psalm-template T * @phpstan-template T
* @template-extends ReadableCollection<TKey, T> * @template-extends ReadableCollection<TKey, T>
* @template-extends ArrayAccess<TKey, T> * @template-extends ArrayAccess<TKey, T>
*/ */
@ -33,11 +35,11 @@ interface Collection extends ReadableCollection, ArrayAccess
* Adds an element at the end of the collection. * Adds an element at the end of the collection.
* *
* @param mixed $element The element to add. * @param mixed $element The element to add.
* @psalm-param T $element * @phpstan-param T $element
* *
* @return true Always TRUE. * @return void we will require a native return type declaration in 3.0
*/ */
public function add($element); public function add(mixed $element);
/** /**
* Clears the collection, removing all elements. * Clears the collection, removing all elements.
@ -50,50 +52,66 @@ interface Collection extends ReadableCollection, ArrayAccess
* Removes the element at the specified index from the collection. * Removes the element at the specified index from the collection.
* *
* @param string|int $key The key/index of the element to remove. * @param string|int $key The key/index of the element to remove.
* @psalm-param TKey $key * @phpstan-param TKey $key
* *
* @return mixed The removed element or NULL, if the collection did not contain the element. * @return mixed The removed element or NULL, if the collection did not contain the element.
* @psalm-return T|null * @phpstan-return T|null
*/ */
public function remove($key); public function remove(string|int $key);
/** /**
* Removes the specified element from the collection, if it is found. * Removes the specified element from the collection, if it is found.
* *
* @param mixed $element The element to remove. * @param mixed $element The element to remove.
* @psalm-param T $element * @phpstan-param T $element
* *
* @return bool TRUE if this collection contained the specified element, FALSE otherwise. * @return bool TRUE if this collection contained the specified element, FALSE otherwise.
*/ */
public function removeElement($element); public function removeElement(mixed $element);
/** /**
* Sets an element in the collection at the specified key/index. * Sets an element in the collection at the specified key/index.
* *
* @param string|int $key The key/index of the element to set. * @param string|int $key The key/index of the element to set.
* @param mixed $value The element to set. * @param mixed $value The element to set.
* @psalm-param TKey $key * @phpstan-param TKey $key
* @psalm-param T $value * @phpstan-param T $value
* *
* @return void * @return void
*/ */
public function set($key, $value); public function set(string|int $key, mixed $value);
/** /**
* {@inheritdoc} * {@inheritDoc}
*
* @phpstan-param Closure(T):U $func
*
* @return Collection<mixed>
* @phpstan-return Collection<TKey, U>
*
* @phpstan-template U
*/
public function map(Closure $func);
/**
* {@inheritDoc}
*
* @phpstan-param Closure(T, TKey):bool $p
* *
* @return Collection<mixed> A collection with the results of the filter operation. * @return Collection<mixed> A collection with the results of the filter operation.
* @psalm-return Collection<TKey, T> * @phpstan-return Collection<TKey, T>
*/ */
public function filter(Closure $p); public function filter(Closure $p);
/** /**
* {@inheritdoc} * {@inheritDoc}
*
* @phpstan-param Closure(TKey, T):bool $p
* *
* @return Collection<mixed>[] An array with two elements. The first element contains the collection * @return Collection<mixed>[] An array with two elements. The first element contains the collection
* of elements where the predicate returned TRUE, the second element * of elements where the predicate returned TRUE, the second element
* contains the collection of elements where the predicate returned FALSE. * contains the collection of elements where the predicate returned FALSE.
* @psalm-return array{0: Collection<TKey, T>, 1: Collection<TKey, T>} * @phpstan-return array{0: Collection<TKey, T>, 1: Collection<TKey, T>}
*/ */
public function partition(Closure $p); public function partition(Closure $p);
} }

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections; namespace Doctrine\Common\Collections;
use Doctrine\Common\Collections\Expr\CompositeExpression; use Doctrine\Common\Collections\Expr\CompositeExpression;
@ -7,43 +9,41 @@ use Doctrine\Common\Collections\Expr\Expression;
use Doctrine\Deprecations\Deprecation; use Doctrine\Deprecations\Deprecation;
use function array_map; use function array_map;
use function func_get_arg;
use function func_num_args; use function func_num_args;
use function strtoupper; use function strtoupper;
/** /**
* Criteria for filtering Selectable collections. * Criteria for filtering Selectable collections.
* *
* @psalm-consistent-constructor * @phpstan-consistent-constructor
*/ */
class Criteria class Criteria
{ {
public const ASC = 'ASC'; /** @deprecated use Order::Ascending instead */
final public const ASC = 'ASC';
public const DESC = 'DESC'; /** @deprecated use Order::Descending instead */
final public const DESC = 'DESC';
/** @var ExpressionBuilder|null */ private static ExpressionBuilder|null $expressionBuilder = null;
private static $expressionBuilder;
/** @var Expression|null */ /** @var array<string, Order> */
private $expression; private array $orderings = [];
/** @var string[] */ private int|null $firstResult = null;
private $orderings = []; private int|null $maxResults = null;
/** @var int|null */
private $firstResult;
/** @var int|null */
private $maxResults;
/** /**
* Creates an instance of the class. * Creates an instance of the class.
* *
* @return Criteria * @return static
*/ */
public static function create() public static function create(/* bool $accessRawFieldValues = false */): self
{ {
return new static(); $accessRawFieldValues = 0 < func_num_args() ? func_get_arg(0) : false;
return new static(firstResult: 0, accessRawFieldValues: $accessRawFieldValues);
} }
/** /**
@ -63,20 +63,30 @@ class Criteria
/** /**
* Construct a new Criteria. * Construct a new Criteria.
* *
* @param string[]|null $orderings * @param array<string, string|Order>|null $orderings
* @param int|null $firstResult
* @param int|null $maxResults
*/ */
public function __construct(?Expression $expression = null, ?array $orderings = null, $firstResult = null, $maxResults = null) public function __construct(
{ private Expression|null $expression = null,
$this->expression = $expression; array|null $orderings = null,
int|null $firstResult = null,
int|null $maxResults = null,
private bool $accessRawFieldValues = false,
) {
if (! $accessRawFieldValues) {
Deprecation::trigger(
'doctrine/collections',
'https://github.com/doctrine/collections/pull/472',
'Not enabling raw field value access for the Criteria matching API in %s is deprecated. Raw field access will be the only supported method in 3.0',
self::class,
);
}
if ($firstResult === null && func_num_args() > 2) { if ($firstResult === null && func_num_args() > 2) {
Deprecation::trigger( Deprecation::trigger(
'doctrine/collections', 'doctrine/collections',
'https://github.com/doctrine/collections/pull/311', 'https://github.com/doctrine/collections/pull/311',
'Passing null as $firstResult to the constructor of %s is deprecated. Pass 0 instead or omit the argument.', 'Passing null as $firstResult to the constructor of %s is deprecated. Pass 0 instead or omit the argument.',
self::class self::class,
); );
} }
@ -116,7 +126,7 @@ class Criteria
$this->expression = new CompositeExpression( $this->expression = new CompositeExpression(
CompositeExpression::TYPE_AND, CompositeExpression::TYPE_AND,
[$this->expression, $expression] [$this->expression, $expression],
); );
return $this; return $this;
@ -136,7 +146,7 @@ class Criteria
$this->expression = new CompositeExpression( $this->expression = new CompositeExpression(
CompositeExpression::TYPE_OR, CompositeExpression::TYPE_OR,
[$this->expression, $expression] [$this->expression, $expression],
); );
return $this; return $this;
@ -155,9 +165,32 @@ class Criteria
/** /**
* Gets the current orderings of this Criteria. * Gets the current orderings of this Criteria.
* *
* @return string[] * @deprecated use orderings() instead
*
* @return array<string, string>
*/ */
public function getOrderings() public function getOrderings()
{
Deprecation::trigger(
'doctrine/collections',
'https://github.com/doctrine/collections/pull/389',
'Calling %s() is deprecated. Use %s::orderings() instead.',
__METHOD__,
self::class,
);
return array_map(
static fn (Order $ordering): string => $ordering->value,
$this->orderings,
);
}
/**
* Gets the current orderings of this Criteria.
*
* @return array<string, Order>
*/
public function orderings(): array
{ {
return $this->orderings; return $this->orderings;
} }
@ -165,22 +198,40 @@ class Criteria
/** /**
* Sets the ordering of the result of this Criteria. * Sets the ordering of the result of this Criteria.
* *
* Keys are field and values are the order, being either ASC or DESC. * Keys are field and values are the order, being a valid Order enum case.
* *
* @see Criteria::ASC * @see Order::Ascending
* @see Criteria::DESC * @see Order::Descending
* *
* @param string[] $orderings * @param array<string, string|Order> $orderings
* *
* @return $this * @return $this
*/ */
public function orderBy(array $orderings) public function orderBy(array $orderings)
{ {
$method = __METHOD__;
$this->orderings = array_map( $this->orderings = array_map(
static function (string $ordering): string { static function (string|Order $ordering) use ($method): Order {
return strtoupper($ordering) === Criteria::ASC ? Criteria::ASC : Criteria::DESC; if ($ordering instanceof Order) {
return $ordering;
}
static $triggered = false;
if (! $triggered) {
Deprecation::trigger(
'doctrine/collections',
'https://github.com/doctrine/collections/pull/389',
'Passing non-Order enum values to %s() is deprecated. Pass Order enum values instead.',
$method,
);
}
$triggered = true;
return strtoupper($ordering) === Order::Ascending->value ? Order::Ascending : Order::Descending;
}, },
$orderings $orderings,
); );
return $this; return $this;
@ -203,14 +254,14 @@ class Criteria
* *
* @return $this * @return $this
*/ */
public function setFirstResult($firstResult) public function setFirstResult(int|null $firstResult)
{ {
if ($firstResult === null) { if ($firstResult === null) {
Deprecation::triggerIfCalledFromOutside( Deprecation::triggerIfCalledFromOutside(
'doctrine/collections', 'doctrine/collections',
'https://github.com/doctrine/collections/pull/311', 'https://github.com/doctrine/collections/pull/311',
'Passing null to %s() is deprecated, pass 0 instead.', 'Passing null to %s() is deprecated, pass 0 instead.',
__METHOD__ __METHOD__,
); );
} }
@ -236,10 +287,16 @@ class Criteria
* *
* @return $this * @return $this
*/ */
public function setMaxResults($maxResults) public function setMaxResults(int|null $maxResults)
{ {
$this->maxResults = $maxResults; $this->maxResults = $maxResults;
return $this; return $this;
} }
/** @internal */
public function isRawFieldValueAccessEnabled(): bool
{
return $this->accessRawFieldValues;
}
} }

View File

@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\Collections\Expr;
use ArrayAccess;
use Closure;
use Doctrine\Deprecations\Deprecation;
use ReflectionClass;
use RuntimeException;
use function array_all;
use function array_any;
use function explode;
use function func_get_arg;
use function func_num_args;
use function in_array;
use function is_array;
use function is_scalar;
use function iterator_to_array;
use function method_exists;
use function preg_match;
use function preg_replace_callback;
use function sprintf;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function strtoupper;
use const PHP_VERSION_ID;
/**
* Walks an expression graph and turns it into a PHP closure.
*
* This closure can be used with {@Collection#filter()} and is used internally
* by {@ArrayCollection#select()}.
*/
class ClosureExpressionVisitor extends ExpressionVisitor
{
public function __construct(
private readonly bool $accessRawFieldValues = false,
) {
}
/**
* Accesses the field of a given object. This field has to be public
* directly or indirectly (through an accessor get*, is*, or a magic
* method, __get, __call).
*
* @param object|mixed[] $object
*
* @return mixed
*/
public static function getObjectFieldValue(object|array $object, string $field, /* bool $accessRawFieldValues = false */)
{
$accessRawFieldValues = 3 <= func_num_args() ? func_get_arg(2) : false;
if (str_contains($field, '.')) {
[$field, $subField] = explode('.', $field, 2);
$object = self::getObjectFieldValue($object, $field, $accessRawFieldValues);
return self::getObjectFieldValue($object, $subField, $accessRawFieldValues);
}
if (is_array($object)) {
return $object[$field];
}
if ($accessRawFieldValues) {
return self::getNearestFieldValue($object, $field);
}
Deprecation::trigger(
'doctrine/collections',
'https://github.com/doctrine/collections/pull/472',
'Not enabling raw field value access for %s is deprecated. Raw field access will be the only supported method in 3.0',
__METHOD__,
);
$accessors = ['get', 'is', ''];
foreach ($accessors as $accessor) {
$accessor .= $field;
if (method_exists($object, $accessor)) {
return $object->$accessor();
}
}
if (preg_match('/^is[A-Z]+/', $field) === 1 && method_exists($object, $field)) {
return $object->$field();
}
// __call should be triggered for get.
$accessor = $accessors[0] . $field;
if (method_exists($object, '__call')) {
return $object->$accessor();
}
if ($object instanceof ArrayAccess) {
return $object[$field];
}
if (isset($object->$field)) {
return $object->$field;
}
// camelcase field name to support different variable naming conventions
$ccField = preg_replace_callback('/_(.?)/', static fn ($matches) => strtoupper((string) $matches[1]), $field);
foreach ($accessors as $accessor) {
$accessor .= $ccField;
if (method_exists($object, $accessor)) {
return $object->$accessor();
}
}
return $object->$field;
}
private static function getNearestFieldValue(object $object, string $field): mixed
{
$reflectionClass = new ReflectionClass($object);
while ($reflectionClass && ! $reflectionClass->hasProperty($field)) {
$reflectionClass = $reflectionClass->getParentClass();
}
if ($reflectionClass === false) {
throw new RuntimeException(sprintf('Field "%s" does not exist in class "%s"', $field, $object::class));
}
$property = $reflectionClass->getProperty($field);
if (PHP_VERSION_ID >= 80400) {
return $property->getRawValue($object);
}
return $property->getValue($object);
}
/**
* Helper for sorting arrays of objects based on multiple fields + orientations.
*
* @return Closure
*/
public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null, /* bool $accessRawFieldValues = false */)
{
$accessRawFieldValues = 4 <= func_num_args() ? func_get_arg(3) : false;
if (! $accessRawFieldValues) {
Deprecation::trigger(
'doctrine/collections',
'https://github.com/doctrine/collections/pull/472',
'Not enabling raw field value access for %s is deprecated. Raw field access will be the only supported method in 3.0',
__METHOD__,
);
}
if (! $next) {
$next = static fn (): int => 0;
}
return static function ($a, $b) use ($name, $next, $orientation, $accessRawFieldValues): int {
$aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name, $accessRawFieldValues);
$bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name, $accessRawFieldValues);
if ($aValue === $bValue) {
return $next($a, $b);
}
return ($aValue > $bValue ? 1 : -1) * $orientation;
};
}
/**
* {@inheritDoc}
*/
public function walkComparison(Comparison $comparison)
{
$field = $comparison->getField();
$value = $comparison->getValue()->getValue();
return match ($comparison->getOperator()) {
Comparison::EQ => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) === $value,
Comparison::NEQ => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) !== $value,
Comparison::LT => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) < $value,
Comparison::LTE => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) <= $value,
Comparison::GT => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) > $value,
Comparison::GTE => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) >= $value,
Comparison::IN => function ($object) use ($field, $value): bool {
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
return in_array($fieldValue, $value, is_scalar($fieldValue));
},
Comparison::NIN => function ($object) use ($field, $value): bool {
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
return ! in_array($fieldValue, $value, is_scalar($fieldValue));
},
Comparison::CONTAINS => fn ($object): bool => str_contains((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
Comparison::MEMBER_OF => function ($object) use ($field, $value): bool {
$fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
if (! is_array($fieldValues)) {
$fieldValues = iterator_to_array($fieldValues);
}
return in_array($value, $fieldValues, true);
},
Comparison::STARTS_WITH => fn ($object): bool => str_starts_with((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
Comparison::ENDS_WITH => fn ($object): bool => str_ends_with((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
default => throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()),
};
}
/**
* {@inheritDoc}
*/
public function walkValue(Value $value)
{
return $value->getValue();
}
/**
* {@inheritDoc}
*/
public function walkCompositeExpression(CompositeExpression $expr)
{
$expressionList = [];
foreach ($expr->getExpressionList() as $child) {
$expressionList[] = $this->dispatch($child);
}
return match ($expr->getType()) {
CompositeExpression::TYPE_AND => $this->andExpressions($expressionList),
CompositeExpression::TYPE_OR => $this->orExpressions($expressionList),
CompositeExpression::TYPE_NOT => $this->notExpression($expressionList),
default => throw new RuntimeException('Unknown composite ' . $expr->getType()),
};
}
/** @param callable[] $expressions */
private function andExpressions(array $expressions): Closure
{
return static fn ($object): bool => array_all(
$expressions,
static fn (callable $expression): bool => (bool) $expression($object),
);
}
/** @param callable[] $expressions */
private function orExpressions(array $expressions): Closure
{
return static fn ($object): bool => array_any(
$expressions,
static fn (callable $expression): bool => (bool) $expression($object),
);
}
/** @param callable[] $expressions */
private function notExpression(array $expressions): Closure
{
return static fn ($object) => ! $expressions[0]($object);
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\Collections\Expr;
/**
* Comparison of a field with a value by the given operator.
*/
class Comparison implements Expression
{
final public const EQ = '=';
final public const NEQ = '<>';
final public const LT = '<';
final public const LTE = '<=';
final public const GT = '>';
final public const GTE = '>=';
final public const IS = '='; // no difference with EQ
final public const IN = 'IN';
final public const NIN = 'NIN';
final public const CONTAINS = 'CONTAINS';
final public const MEMBER_OF = 'MEMBER_OF';
final public const STARTS_WITH = 'STARTS_WITH';
final public const ENDS_WITH = 'ENDS_WITH';
private readonly Value $value;
public function __construct(private readonly string $field, private readonly string $op, mixed $value)
{
if (! ($value instanceof Value)) {
$value = new Value($value);
}
$this->value = $value;
}
/** @return string */
public function getField()
{
return $this->field;
}
/** @return Value */
public function getValue()
{
return $this->value;
}
/** @return string */
public function getOperator()
{
return $this->op;
}
/**
* {@inheritDoc}
*/
public function visit(ExpressionVisitor $visitor)
{
return $visitor->walkComparison($this);
}
}

View File

@ -1,33 +1,32 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections\Expr; namespace Doctrine\Common\Collections\Expr;
use RuntimeException; use RuntimeException;
use function count;
/** /**
* Expression of Expressions combined by AND or OR operation. * Expression of Expressions combined by AND or OR operation.
*/ */
class CompositeExpression implements Expression class CompositeExpression implements Expression
{ {
public const TYPE_AND = 'AND'; final public const TYPE_AND = 'AND';
public const TYPE_OR = 'OR'; final public const TYPE_OR = 'OR';
final public const TYPE_NOT = 'NOT';
/** @var string */ /** @var list<Expression> */
private $type; private array $expressions = [];
/** @var Expression[] */
private $expressions = [];
/** /**
* @param string $type * @param Expression[] $expressions
* @param mixed[] $expressions
* *
* @throws RuntimeException * @throws RuntimeException
*/ */
public function __construct($type, array $expressions) public function __construct(private readonly string $type, array $expressions)
{ {
$this->type = $type;
foreach ($expressions as $expr) { foreach ($expressions as $expr) {
if ($expr instanceof Value) { if ($expr instanceof Value) {
throw new RuntimeException('Values are not supported expressions as children of and/or expressions.'); throw new RuntimeException('Values are not supported expressions as children of and/or expressions.');
@ -39,12 +38,16 @@ class CompositeExpression implements Expression
$this->expressions[] = $expr; $this->expressions[] = $expr;
} }
if ($type === self::TYPE_NOT && count($this->expressions) !== 1) {
throw new RuntimeException('Not expression only allows one expression as child.');
}
} }
/** /**
* Returns the list of expressions nested in this composite. * Returns the list of expressions nested in this composite.
* *
* @return Expression[] * @return list<Expression>
*/ */
public function getExpressionList() public function getExpressionList()
{ {

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections\Expr; namespace Doctrine\Common\Collections\Expr;
/** /**

View File

@ -1,11 +1,9 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections\Expr; namespace Doctrine\Common\Collections\Expr;
use RuntimeException;
use function get_class;
/** /**
* An Expression visitor walks a graph of expressions and turns them into a * An Expression visitor walks a graph of expressions and turns them into a
* query for the underlying implementation. * query for the underlying implementation.
@ -37,23 +35,9 @@ abstract class ExpressionVisitor
* Dispatches walking an expression to the appropriate handler. * Dispatches walking an expression to the appropriate handler.
* *
* @return mixed * @return mixed
*
* @throws RuntimeException
*/ */
public function dispatch(Expression $expr) public function dispatch(Expression $expr)
{ {
switch (true) { return $expr->visit($this);
case $expr instanceof Comparison:
return $this->walkComparison($expr);
case $expr instanceof Value:
return $this->walkValue($expr);
case $expr instanceof CompositeExpression:
return $this->walkCompositeExpression($expr);
default:
throw new RuntimeException('Unknown Expression ' . get_class($expr));
}
} }
} }

View File

@ -1,16 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections\Expr; namespace Doctrine\Common\Collections\Expr;
class Value implements Expression class Value implements Expression
{ {
/** @var mixed */ public function __construct(private readonly mixed $value)
private $value;
/** @param mixed $value */
public function __construct($value)
{ {
$this->value = $value;
} }
/** @return mixed */ /** @return mixed */

View File

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\Collections;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\Common\Collections\Expr\CompositeExpression;
use Doctrine\Common\Collections\Expr\Expression;
use Doctrine\Common\Collections\Expr\Value;
/**
* Builder for Expressions in the {@link Selectable} interface.
*
* Important Notice for interoperable code: You have to use scalar
* values only for comparisons, otherwise the behavior of the comparison
* may be different between implementations (Array vs ORM vs ODM).
*/
class ExpressionBuilder
{
/** @return CompositeExpression */
public function andX(Expression ...$expressions)
{
return new CompositeExpression(CompositeExpression::TYPE_AND, $expressions);
}
/** @return CompositeExpression */
public function orX(Expression ...$expressions)
{
return new CompositeExpression(CompositeExpression::TYPE_OR, $expressions);
}
public function not(Expression $expression): CompositeExpression
{
return new CompositeExpression(CompositeExpression::TYPE_NOT, [$expression]);
}
/** @return Comparison */
public function eq(string $field, mixed $value)
{
return new Comparison($field, Comparison::EQ, new Value($value));
}
/** @return Comparison */
public function gt(string $field, mixed $value)
{
return new Comparison($field, Comparison::GT, new Value($value));
}
/** @return Comparison */
public function lt(string $field, mixed $value)
{
return new Comparison($field, Comparison::LT, new Value($value));
}
/** @return Comparison */
public function gte(string $field, mixed $value)
{
return new Comparison($field, Comparison::GTE, new Value($value));
}
/** @return Comparison */
public function lte(string $field, mixed $value)
{
return new Comparison($field, Comparison::LTE, new Value($value));
}
/** @return Comparison */
public function neq(string $field, mixed $value)
{
return new Comparison($field, Comparison::NEQ, new Value($value));
}
/** @return Comparison */
public function isNull(string $field)
{
return new Comparison($field, Comparison::EQ, new Value(null));
}
public function isNotNull(string $field): Comparison
{
return new Comparison($field, Comparison::NEQ, new Value(null));
}
/**
* @param mixed[] $values
*
* @return Comparison
*/
public function in(string $field, array $values)
{
return new Comparison($field, Comparison::IN, new Value($values));
}
/**
* @param mixed[] $values
*
* @return Comparison
*/
public function notIn(string $field, array $values)
{
return new Comparison($field, Comparison::NIN, new Value($values));
}
/** @return Comparison */
public function contains(string $field, mixed $value)
{
return new Comparison($field, Comparison::CONTAINS, new Value($value));
}
/** @return Comparison */
public function memberOf(string $field, mixed $value)
{
return new Comparison($field, Comparison::MEMBER_OF, new Value($value));
}
/** @return Comparison */
public function startsWith(string $field, mixed $value)
{
return new Comparison($field, Comparison::STARTS_WITH, new Value($value));
}
/** @return Comparison */
public function endsWith(string $field, mixed $value)
{
return new Comparison($field, Comparison::ENDS_WITH, new Value($value));
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\Collections;
enum Order: string
{
case Ascending = 'ASC';
case Descending = 'DESC';
}

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections; namespace Doctrine\Common\Collections;
use Closure; use Closure;
@ -7,7 +9,7 @@ use Countable;
use IteratorAggregate; use IteratorAggregate;
/** /**
* @psalm-template TKey of array-key * @phpstan-template TKey of array-key
* @template-covariant T * @template-covariant T
* @template-extends IteratorAggregate<TKey, T> * @template-extends IteratorAggregate<TKey, T>
*/ */
@ -18,14 +20,14 @@ interface ReadableCollection extends Countable, IteratorAggregate
* This is an O(n) operation, where n is the size of the collection. * This is an O(n) operation, where n is the size of the collection.
* *
* @param mixed $element The element to search for. * @param mixed $element The element to search for.
* @psalm-param TMaybeContained $element * @phpstan-param TMaybeContained $element
* *
* @return bool TRUE if the collection contains the element, FALSE otherwise. * @return bool TRUE if the collection contains the element, FALSE otherwise.
* @psalm-return (TMaybeContained is T ? bool : false) * @phpstan-return (TMaybeContained is T ? bool : false)
* *
* @template TMaybeContained * @template TMaybeContained
*/ */
public function contains($element); public function contains(mixed $element);
/** /**
* Checks whether the collection is empty (contains no elements). * Checks whether the collection is empty (contains no elements).
@ -38,30 +40,30 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Checks whether the collection contains an element with the specified key/index. * Checks whether the collection contains an element with the specified key/index.
* *
* @param string|int $key The key/index to check for. * @param string|int $key The key/index to check for.
* @psalm-param TKey $key * @phpstan-param TKey $key
* *
* @return bool TRUE if the collection contains an element with the specified key/index, * @return bool TRUE if the collection contains an element with the specified key/index,
* FALSE otherwise. * FALSE otherwise.
*/ */
public function containsKey($key); public function containsKey(string|int $key);
/** /**
* Gets the element at the specified key/index. * Gets the element at the specified key/index.
* *
* @param string|int $key The key/index of the element to retrieve. * @param string|int $key The key/index of the element to retrieve.
* @psalm-param TKey $key * @phpstan-param TKey $key
* *
* @return mixed * @return mixed
* @psalm-return T|null * @phpstan-return T|null
*/ */
public function get($key); public function get(string|int $key);
/** /**
* Gets all keys/indices of the collection. * Gets all keys/indices of the collection.
* *
* @return int[]|string[] The keys/indices of the collection, in the order of the corresponding * @return int[]|string[] The keys/indices of the collection, in the order of the corresponding
* elements in the collection. * elements in the collection.
* @psalm-return list<TKey> * @phpstan-return list<TKey>
*/ */
public function getKeys(); public function getKeys();
@ -70,7 +72,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* *
* @return mixed[] The values of all elements in the collection, in the * @return mixed[] The values of all elements in the collection, in the
* order they appear in the collection. * order they appear in the collection.
* @psalm-return list<T> * @phpstan-return list<T>
*/ */
public function getValues(); public function getValues();
@ -78,7 +80,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Gets a native PHP array representation of the collection. * Gets a native PHP array representation of the collection.
* *
* @return mixed[] * @return mixed[]
* @psalm-return array<TKey,T> * @phpstan-return array<TKey,T>
*/ */
public function toArray(); public function toArray();
@ -86,7 +88,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Sets the internal iterator to the first element in the collection and returns this element. * Sets the internal iterator to the first element in the collection and returns this element.
* *
* @return mixed * @return mixed
* @psalm-return T|false * @phpstan-return T|false
*/ */
public function first(); public function first();
@ -94,7 +96,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Sets the internal iterator to the last element in the collection and returns this element. * Sets the internal iterator to the last element in the collection and returns this element.
* *
* @return mixed * @return mixed
* @psalm-return T|false * @phpstan-return T|false
*/ */
public function last(); public function last();
@ -102,7 +104,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Gets the key/index of the element at the current iterator position. * Gets the key/index of the element at the current iterator position.
* *
* @return int|string|null * @return int|string|null
* @psalm-return TKey|null * @phpstan-return TKey|null
*/ */
public function key(); public function key();
@ -110,7 +112,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Gets the element of the collection at the current iterator position. * Gets the element of the collection at the current iterator position.
* *
* @return mixed * @return mixed
* @psalm-return T|false * @phpstan-return T|false
*/ */
public function current(); public function current();
@ -118,7 +120,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Moves the internal iterator position to the next element and returns this element. * Moves the internal iterator position to the next element and returns this element.
* *
* @return mixed * @return mixed
* @psalm-return T|false * @phpstan-return T|false
*/ */
public function next(); public function next();
@ -133,15 +135,15 @@ interface ReadableCollection extends Countable, IteratorAggregate
* @param int|null $length The maximum number of elements to return, or null for no limit. * @param int|null $length The maximum number of elements to return, or null for no limit.
* *
* @return mixed[] * @return mixed[]
* @psalm-return array<TKey,T> * @phpstan-return array<TKey,T>
*/ */
public function slice($offset, $length = null); public function slice(int $offset, int|null $length = null);
/** /**
* Tests for the existence of an element that satisfies the given predicate. * Tests for the existence of an element that satisfies the given predicate.
* *
* @param Closure $p The predicate. * @param Closure $p The predicate.
* @psalm-param Closure(TKey, T):bool $p * @phpstan-param Closure(TKey, T):bool $p
* *
* @return bool TRUE if the predicate is TRUE for at least one element, FALSE otherwise. * @return bool TRUE if the predicate is TRUE for at least one element, FALSE otherwise.
*/ */
@ -152,10 +154,10 @@ interface ReadableCollection extends Countable, IteratorAggregate
* The order of the elements is preserved. * The order of the elements is preserved.
* *
* @param Closure $p The predicate used for filtering. * @param Closure $p The predicate used for filtering.
* @psalm-param Closure(T):bool $p * @phpstan-param Closure(T, TKey):bool $p
* *
* @return ReadableCollection<mixed> A collection with the results of the filter operation. * @return ReadableCollection<mixed> A collection with the results of the filter operation.
* @psalm-return ReadableCollection<TKey, T> * @phpstan-return ReadableCollection<TKey, T>
*/ */
public function filter(Closure $p); public function filter(Closure $p);
@ -163,12 +165,12 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Applies the given function to each element in the collection and returns * Applies the given function to each element in the collection and returns
* a new collection with the elements returned by the function. * a new collection with the elements returned by the function.
* *
* @psalm-param Closure(T):U $func * @phpstan-param Closure(T):U $func
* *
* @return Collection<mixed> * @return ReadableCollection<mixed>
* @psalm-return Collection<TKey, U> * @phpstan-return ReadableCollection<TKey, U>
* *
* @psalm-template U * @phpstan-template U
*/ */
public function map(Closure $func); public function map(Closure $func);
@ -177,12 +179,12 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Keys are preserved in the resulting collections. * Keys are preserved in the resulting collections.
* *
* @param Closure $p The predicate on which to partition. * @param Closure $p The predicate on which to partition.
* @psalm-param Closure(TKey, T):bool $p * @phpstan-param Closure(TKey, T):bool $p
* *
* @return ReadableCollection<mixed>[] An array with two elements. The first element contains the collection * @return ReadableCollection<mixed>[] An array with two elements. The first element contains the collection
* of elements where the predicate returned TRUE, the second element * of elements where the predicate returned TRUE, the second element
* contains the collection of elements where the predicate returned FALSE. * contains the collection of elements where the predicate returned FALSE.
* @psalm-return array{0: ReadableCollection<TKey, T>, 1: ReadableCollection<TKey, T>} * @phpstan-return array{0: ReadableCollection<TKey, T>, 1: ReadableCollection<TKey, T>}
*/ */
public function partition(Closure $p); public function partition(Closure $p);
@ -190,7 +192,7 @@ interface ReadableCollection extends Countable, IteratorAggregate
* Tests whether the given predicate p holds for all elements of this collection. * Tests whether the given predicate p holds for all elements of this collection.
* *
* @param Closure $p The predicate. * @param Closure $p The predicate.
* @psalm-param Closure(TKey, T):bool $p * @phpstan-param Closure(TKey, T):bool $p
* *
* @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise.
*/ */
@ -202,12 +204,39 @@ interface ReadableCollection extends Countable, IteratorAggregate
* For objects this means reference equality. * For objects this means reference equality.
* *
* @param mixed $element The element to search for. * @param mixed $element The element to search for.
* @psalm-param TMaybeContained $element * @phpstan-param TMaybeContained $element
* *
* @return int|string|bool The key/index of the element or FALSE if the element was not found. * @return int|string|bool The key/index of the element or FALSE if the element was not found.
* @psalm-return (TMaybeContained is T ? TKey|false : false) * @phpstan-return (TMaybeContained is T ? TKey|false : false)
* *
* @template TMaybeContained * @template TMaybeContained
*/ */
public function indexOf($element); public function indexOf(mixed $element);
/**
* Returns the first element of this collection that satisfies the predicate p.
*
* @param Closure $p The predicate.
* @phpstan-param Closure(TKey, T):bool $p
*
* @return mixed The first element respecting the predicate,
* null if no element respects the predicate.
* @phpstan-return T|null
*/
public function findFirst(Closure $p);
/**
* Applies iteratively the given function to each element in the collection,
* so as to reduce the collection to a single value.
*
* @phpstan-param Closure(TReturn|TInitial, T):TReturn $func
* @phpstan-param TInitial $initial
*
* @return mixed
* @phpstan-return TReturn|TInitial
*
* @phpstan-template TReturn
* @phpstan-template TInitial
*/
public function reduce(Closure $func, mixed $initial = null);
} }

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace Doctrine\Common\Collections; namespace Doctrine\Common\Collections;
/** /**
@ -14,17 +16,17 @@ namespace Doctrine\Common\Collections;
* this API can implement efficient database access without having to ask the * this API can implement efficient database access without having to ask the
* EntityManager or Repositories. * EntityManager or Repositories.
* *
* @psalm-template TKey as array-key * @phpstan-template TKey as array-key
* @psalm-template T * @phpstan-template-covariant T
*/ */
interface Selectable interface Selectable
{ {
/** /**
* Selects all elements from a selectable that match the expression and * Selects all elements from a selectable that match the expression and
* returns a new collection containing these elements. * returns a new collection containing these elements and preserved keys.
* *
* @return Collection<mixed>&Selectable<mixed> * @return ReadableCollection<mixed>&Selectable<mixed>
* @psalm-return Collection<TKey,T>&Selectable<TKey,T> * @phpstan-return ReadableCollection<TKey,T>&Selectable<TKey,T>
*/ */
public function matching(Criteria $criteria); public function matching(Criteria $criteria);
} }

View File

@ -12,13 +12,12 @@
} }
], ],
"require": { "require": {
"php": "^7.2|^8.0", "php": "^8.2|^8.3|^8.4|^8.5"
"webmozart/assert": "^1.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.0", "phpstan/phpstan": "^1.12.32|^2.1.31",
"phpunit/phpunit": "^7.0|^8.0|^9.0", "phpunit/phpunit": "^8.5.48|^9.0",
"phpstan/extension-installer": "^1.0" "phpstan/extension-installer": "^1.4.3"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -14,14 +14,14 @@ abstract class AbstractField implements FieldInterface
/** /**
* Full range of values that are allowed for this field type. * Full range of values that are allowed for this field type.
* *
* @var array * @var array<int, int>
*/ */
protected $fullRange = []; protected $fullRange = [];
/** /**
* Literal values we need to convert to integers. * Literal values we need to convert to integers.
* *
* @var array * @var array<int, string>
*/ */
protected $literals = []; protected $literals = [];
@ -134,7 +134,6 @@ abstract class AbstractField implements FieldInterface
$step = $chunks[1] ?? 0; $step = $chunks[1] ?? 0;
// No step or 0 steps aren't cool // No step or 0 steps aren't cool
/** @phpstan-ignore-next-line */
if (null === $step || '0' === $step || 0 === $step) { if (null === $step || '0' === $step || 0 === $step) {
return false; return false;
} }
@ -167,7 +166,7 @@ abstract class AbstractField implements FieldInterface
// we will not wrap around. However, because the logic exists today // we will not wrap around. However, because the logic exists today
// per the above documentation, fixing the bug from #89 // per the above documentation, fixing the bug from #89
if ($step > $this->rangeEnd) { if ($step > $this->rangeEnd) {
$thisRange = [$this->fullRange[$step % \count($this->fullRange)]]; $thisRange = [$this->fullRange[(int) $step % \count($this->fullRange)]];
} else { } else {
if ($step > ($rangeEnd - $rangeStart)) { if ($step > ($rangeEnd - $rangeStart)) {
$thisRange[$rangeStart] = (int) $rangeStart; $thisRange[$rangeStart] = (int) $rangeStart;
@ -185,7 +184,7 @@ abstract class AbstractField implements FieldInterface
* @param string $expression The expression to evaluate * @param string $expression The expression to evaluate
* @param int $max Maximum offset for range * @param int $max Maximum offset for range
* *
* @return array * @return array<int, int>
*/ */
public function getRangeForExpression(string $expression, int $max): array public function getRangeForExpression(string $expression, int $max): array
{ {
@ -219,7 +218,7 @@ abstract class AbstractField implements FieldInterface
} }
$offset = '*' === $offset ? $this->rangeStart : $offset; $offset = '*' === $offset ? $this->rangeStart : $offset;
if ($stepSize >= $this->rangeEnd) { if ($stepSize >= $this->rangeEnd) {
$values = [$this->fullRange[$stepSize % \count($this->fullRange)]]; $values = [$this->fullRange[(int) $stepSize % \count($this->fullRange)]];
} else { } else {
for ($i = $offset; $i <= $to; $i += $stepSize) { for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = (int) $i; $values[] = (int) $i;

View File

@ -47,7 +47,7 @@ class CronExpression
]; ];
/** /**
* @var array CRON expression parts * @var array<int, string> CRON expression parts
*/ */
protected $cronParts; protected $cronParts;
@ -62,7 +62,7 @@ class CronExpression
protected $maxIterationCount = 1000; protected $maxIterationCount = 1000;
/** /**
* @var array Order in which to test of cron parts * @var array<int, int> Order in which to test of cron parts
*/ */
protected static $order = [ protected static $order = [
self::YEAR, self::YEAR,

View File

@ -33,7 +33,7 @@ class DayOfWeekField extends AbstractField
protected $rangeEnd = 7; protected $rangeEnd = 7;
/** /**
* @var array Weekday range * @var array<int, int> Weekday range
*/ */
protected $nthRange; protected $nthRange;
@ -69,7 +69,7 @@ class DayOfWeekField extends AbstractField
// Find out if this is the last specific weekday of the month // Find out if this is the last specific weekday of the month
if ($lPosition = strpos($value, 'L')) { if ($lPosition = strpos($value, 'L')) {
$weekday = $this->convertLiterals(substr($value, 0, $lPosition)); $weekday = (int) $this->convertLiterals(substr($value, 0, $lPosition));
$weekday %= 7; $weekday %= 7;
$daysInMonth = (int) $date->format('t'); $daysInMonth = (int) $date->format('t');

View File

@ -14,7 +14,7 @@ use InvalidArgumentException;
class FieldFactory implements FieldFactoryInterface class FieldFactory implements FieldFactoryInterface
{ {
/** /**
* @var array Cache of instantiated fields * @var array<int, FieldInterface> Cache of instantiated fields
*/ */
private $fields = []; private $fields = [];

View File

@ -23,7 +23,7 @@ class HoursField extends AbstractField
protected $rangeEnd = 23; protected $rangeEnd = 23;
/** /**
* @var array|null Transitions returned by DateTimeZone::getTransitions() * @var list<array<string, bool|int|string>>|null Transitions returned by DateTimeZone::getTransitions()
*/ */
protected $transitions = []; protected $transitions = [];
@ -73,6 +73,9 @@ class HoursField extends AbstractField
return $retval; return $retval;
} }
/**
* @return non-empty-array<string, bool|int|string>|null
*/
public function getPastTransition(DateTimeInterface $date): ?array public function getPastTransition(DateTimeInterface $date): ?array
{ {
$currentTimestamp = (int) $date->format('U'); $currentTimestamp = (int) $date->format('U');
@ -160,6 +163,7 @@ class HoursField extends AbstractField
$target = (int) $hours[$position]; $target = (int) $hours[$position];
$originalHour = (int)$date->format('H'); $originalHour = (int)$date->format('H');
$originalDst = (int)$date->format('I');
$originalDay = (int)$date->format('d'); $originalDay = (int)$date->format('d');
$previousOffset = $date->getOffset(); $previousOffset = $date->getOffset();
@ -200,6 +204,11 @@ class HoursField extends AbstractField
$date = $this->timezoneSafeModify($date, "-{$distance} hours"); $date = $this->timezoneSafeModify($date, "-{$distance} hours");
} }
$actualDst = (int)$date->format('I');
if ($originalDst < $actualDst) {
$date = $this->timezoneSafeModify($date, "-1 hours");
}
$date = $this->setTimeHour($date, $invert, $originalTimestamp); $date = $this->setTimeHour($date, $invert, $originalTimestamp);
$actualHour = (int)$date->format('H'); $actualHour = (int)$date->format('H');

View File

@ -9,7 +9,7 @@
"sort-packages": true "sort-packages": true
}, },
"require": { "require": {
"php": ">=7.2", "php": ">=8.2",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"ipl/stdlib": ">=0.12.0", "ipl/stdlib": ">=0.12.0",
"ipl/validator": ">=0.5.0", "ipl/validator": ">=0.5.0",
@ -17,6 +17,7 @@
"guzzlehttp/psr7": "^2.5" "guzzlehttp/psr7": "^2.5"
}, },
"require-dev": { "require-dev": {
"ext-dom": "*",
"ipl/stdlib": "dev-main", "ipl/stdlib": "dev-main",
"ipl/validator": "dev-main" "ipl/validator": "dev-main"
}, },

View File

@ -12,6 +12,9 @@ use InvalidArgumentException;
* *
* Usually attributes are not instantiated directly, but created through an HTML * Usually attributes are not instantiated directly, but created through an HTML
* element's exposed methods. * element's exposed methods.
*
* @phpstan-type _AttributeScalar string|bool|null
* @phpstan-type AttributeValue _AttributeScalar|array<_AttributeScalar>
*/ */
class Attribute class Attribute
{ {
@ -21,14 +24,14 @@ class Attribute
/** @var string The separator used if value is an array */ /** @var string The separator used if value is an array */
protected $separator = ' '; protected $separator = ' ';
/** @var string|array|bool|null */ /** @var AttributeValue */
protected $value; protected $value;
/** /**
* Create a new HTML attribute from the given name and value * Create a new HTML attribute from the given name and value
* *
* @param string $name The name of the attribute * @param string $name The name of the attribute
* @param string|bool|array|null $value The value of the attribute * @param AttributeValue $value The value of the attribute
* *
* @throws InvalidArgumentException If the name of the attribute contains special characters * @throws InvalidArgumentException If the name of the attribute contains special characters
*/ */
@ -40,8 +43,8 @@ class Attribute
/** /**
* Create a new HTML attribute from the given name and value * Create a new HTML attribute from the given name and value
* *
* @param string $name The name of the attribute * @param string $name The name of the attribute
* @param string|bool|array|null $value The value of the attribute * @param AttributeValue $value The value of the attribute
* *
* @return static * @return static
* *
@ -68,6 +71,28 @@ class Attribute
return new static($name, null); return new static($name, null);
} }
/**
* Sanitize the given ID
*
* Removes all characters that are not alphanumeric, underscore or hyphen. Additionally,
* the result is enforced to not start with a number by prepending the letter 'a'.
* Use it if you need to ensure that an ID is safe to use in CSS selectors.
*
* @param non-empty-string $id
*
* @return string
*
* @throws InvalidArgumentException If the ID is an empty string
*/
public static function sanitizeId(string $id): string
{
if (empty($id)) {
throw new InvalidArgumentException('ID must not be empty');
}
return 'a' . preg_replace('/[^a-z0-9_\-]/i', '', $id);
}
/** /**
* Escape the name of an attribute * Escape the name of an attribute
* *
@ -91,8 +116,8 @@ class Attribute
* Values are escaped according to the HTML5 double-quoted attribute value syntax: * Values are escaped according to the HTML5 double-quoted attribute value syntax:
* {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 }. * {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 }.
* *
* @param string|array $value * @param string|string[] $value
* @param string $glue Glue string to join elements if value is an array * @param string $glue Glue string to join elements if value is an array
* *
* @return string * @return string
*/ */
@ -184,7 +209,7 @@ class Attribute
/** /**
* Get the value of the attribute * Get the value of the attribute
* *
* @return string|bool|array|null * @return AttributeValue
*/ */
public function getValue() public function getValue()
{ {
@ -194,7 +219,7 @@ class Attribute
/** /**
* Set the value of the attribute * Set the value of the attribute
* *
* @param string|bool|array|null $value * @param AttributeValue $value
* *
* @return $this * @return $this
*/ */
@ -208,7 +233,7 @@ class Attribute
/** /**
* Add the given value(s) to the attribute * Add the given value(s) to the attribute
* *
* @param string|array $value The value(s) to add * @param AttributeValue $value The value(s) to add
* *
* @return $this * @return $this
*/ */
@ -230,7 +255,7 @@ class Attribute
* *
* Does nothing if there is no such value to remove. * Does nothing if there is no such value to remove.
* *
* @param string|array $value The value(s) to remove * @param AttributeValue $value The value(s) to remove
* *
* @return $this * @return $this
*/ */

View File

@ -17,6 +17,9 @@ use function ipl\Stdlib\get_php_type;
* behavior in various ways. * behavior in various ways.
* *
* Attributes usually come in name-value pairs and are rendered as name="value". * Attributes usually come in name-value pairs and are rendered as name="value".
*
* @phpstan-import-type AttributeValue from Attribute
* @phpstan-type AttributesType array<string, AttributeValue>
*/ */
class Attributes implements ArrayAccess, IteratorAggregate class Attributes implements ArrayAccess, IteratorAggregate
{ {
@ -35,7 +38,7 @@ class Attributes implements ArrayAccess, IteratorAggregate
/** /**
* Create new HTML attributes * Create new HTML attributes
* *
* @param array $attributes * @param AttributesType $attributes
*/ */
public function __construct(array $attributes = null) public function __construct(array $attributes = null)
{ {
@ -57,7 +60,7 @@ class Attributes implements ArrayAccess, IteratorAggregate
/** /**
* Create new HTML attributes * Create new HTML attributes
* *
* @param array $attributes * @param AttributesType $attributes
* *
* @return static * @return static
*/ */
@ -76,7 +79,7 @@ class Attributes implements ArrayAccess, IteratorAggregate
* construct and return a new Attributes instance. * construct and return a new Attributes instance.
* If the attributes are null, an empty new instance of Attributes is returned. * If the attributes are null, an empty new instance of Attributes is returned.
* *
* @param array|static|null $attributes * @param AttributesType|static|null $attributes
* *
* @return static * @return static
* *
@ -174,8 +177,8 @@ class Attributes implements ArrayAccess, IteratorAggregate
* *
* If the attribute with the given name already exists, it gets overridden. * If the attribute with the given name already exists, it gets overridden.
* *
* @param string|array|Attribute|self $attribute The attribute(s) to add * @param string|AttributesType|Attribute|self $attribute The attribute(s) to add
* @param string|bool|array $value The value of the attribute * @param AttributeValue $value The value of the attribute
* *
* @return $this * @return $this
* *
@ -224,8 +227,8 @@ class Attributes implements ArrayAccess, IteratorAggregate
* If an attribute with the same name already exists, the attribute's value will be added to the current value of * If an attribute with the same name already exists, the attribute's value will be added to the current value of
* the attribute. * the attribute.
* *
* @param string|array|Attribute|self $attribute The attribute(s) to add * @param string|AttributesType|Attribute|self $attribute The attribute(s) to add
* @param string|bool|array $value The value of the attribute * @param AttributeValue $value The value of the attribute
* *
* @return $this * @return $this
* *
@ -246,6 +249,7 @@ class Attributes implements ArrayAccess, IteratorAggregate
} }
if (is_array($attribute)) { if (is_array($attribute)) {
// TODO: Handle if $attribute = [new Attribute('class', 'bar')]
foreach ($attribute as $name => $value) { foreach ($attribute as $name => $value) {
$this->add($name, $value); $this->add($name, $value);
} }
@ -280,7 +284,7 @@ class Attributes implements ArrayAccess, IteratorAggregate
* Remove the attribute with the given name or remove the given value from the attribute * Remove the attribute with the given name or remove the given value from the attribute
* *
* @param string $name The name of the attribute * @param string $name The name of the attribute
* @param null|string|array $value The value to remove if specified * @param AttributeValue $value The value to remove if specified
* *
* @return ?Attribute The removed or changed attribute, if any, otherwise null * @return ?Attribute The removed or changed attribute, if any, otherwise null
*/ */
@ -479,8 +483,8 @@ class Attributes implements ArrayAccess, IteratorAggregate
* *
* If the attribute with the given name already exists, it gets overridden. * If the attribute with the given name already exists, it gets overridden.
* *
* @param string $name Name of the attribute * @param string $name Name of the attribute
* @param mixed $value Value of the attribute * @param AttributeValue $value Value of the attribute
* *
* @throws InvalidArgumentException If the attribute name contains special characters * @throws InvalidArgumentException If the attribute name contains special characters
*/ */

View File

@ -2,7 +2,7 @@
namespace ipl\Html; namespace ipl\Html;
use InvalidArgumentException; use ipl\Html\Contract\HtmlElementInterface;
use RuntimeException; use RuntimeException;
/** /**
@ -32,7 +32,7 @@ use RuntimeException;
* } * }
* ``` * ```
*/ */
abstract class BaseHtmlElement extends HtmlDocument abstract class BaseHtmlElement extends HtmlDocument implements HtmlElementInterface
{ {
/** /**
* List of void elements which must not contain end tags or content * List of void elements which must not contain end tags or content
@ -76,11 +76,6 @@ abstract class BaseHtmlElement extends HtmlDocument
/** @var string Tag of element. Set this property in order to provide the element's tag when extending this class */ /** @var string Tag of element. Set this property in order to provide the element's tag when extending this class */
protected $tag; protected $tag;
/**
* Get the attributes of the element
*
* @return Attributes
*/
public function getAttributes() public function getAttributes()
{ {
if ($this->attributes === null) { if ($this->attributes === null) {
@ -97,6 +92,13 @@ abstract class BaseHtmlElement extends HtmlDocument
return $this->attributes; return $this->attributes;
} }
public function addAttributes($attributes)
{
$this->getAttributes()->add($attributes);
return $this;
}
/** /**
* Set the attributes of the element * Set the attributes of the element
* *
@ -114,44 +116,16 @@ abstract class BaseHtmlElement extends HtmlDocument
return $this; return $this;
} }
/**
* Return true if the attribute with the given name exists, false otherwise
*
* @param string $name
*
* @return bool
*/
public function hasAttribute(string $name): bool public function hasAttribute(string $name): bool
{ {
return $this->getAttributes()->has($name); return $this->getAttributes()->has($name);
} }
/**
* Get the attribute with the given name
*
* If the attribute does not already exist, an empty one is automatically created and added to the attributes.
*
* @param string $name
*
* @return Attribute
*
* @throws InvalidArgumentException If the attribute does not yet exist and its name contains special characters
*/
public function getAttribute(string $name): Attribute public function getAttribute(string $name): Attribute
{ {
return $this->getAttributes()->get($name); return $this->getAttributes()->get($name);
} }
/**
* Set the attribute with the given name and value
*
* If the attribute with the given name already exists, it gets overridden.
*
* @param string $name The name of the attribute
* @param string|bool|array $value The value of the attribute
*
* @return $this
*/
public function setAttribute($name, $value) public function setAttribute($name, $value)
{ {
$this->getAttributes()->set($name, $value); $this->getAttributes()->set($name, $value);
@ -159,33 +133,11 @@ abstract class BaseHtmlElement extends HtmlDocument
return $this; return $this;
} }
/**
* Remove the attribute with the given name or remove the given value from the attribute
*
* @param string $name The name of the attribute
* @param null|string|array $value The value to remove if specified
*
* @return ?Attribute The removed or changed attribute, if any, otherwise null
*/
public function removeAttribute(string $name, $value = null): ?Attribute public function removeAttribute(string $name, $value = null): ?Attribute
{ {
return $this->getAttributes()->remove($name, $value); return $this->getAttributes()->remove($name, $value);
} }
/**
* Add the given attributes
*
* @param Attributes|array $attributes
*
* @return $this
*/
public function addAttributes($attributes)
{
$this->getAttributes()->add($attributes);
return $this;
}
/** /**
* Get the default attributes of the element * Get the default attributes of the element
* *

View File

@ -0,0 +1,32 @@
<?php
namespace ipl\Html\Contract;
use ipl\Html\FormDecoration\DecoratorChain;
/**
* Representation of form elements that support decoration
*/
interface DecorableFormElement
{
/**
* Get all decorators of this element
*
* @return DecoratorChain<FormElementDecoration>
*/
public function getDecorators(): DecoratorChain;
/**
* Get whether the element has any decorators
*
* @return bool
*/
public function hasDecorators(): bool;
/**
* Decorate the element using its decorators
*
* @return void
*/
public function applyDecoration(): void;
}

View File

@ -0,0 +1,35 @@
<?php
namespace ipl\Html\Contract;
use ipl\Html\ValidHtml;
interface DecorationResult
{
/**
* Add the given HTML to the end of the result
*
* @param ValidHtml $html The HTML to add
*
* @return $this
*/
public function append(ValidHtml $html): static;
/**
* Prepend the given HTML to the beginning of the result
*
* @param ValidHtml $html The HTML to prepend
*
* @return $this
*/
public function prepend(ValidHtml $html): static;
/**
* Set the given HTML as the container of the result
*
* @param MutableHtml $html The container
*
* @return $this
*/
public function wrap(MutableHtml $html): static;
}

View File

@ -0,0 +1,40 @@
<?php
namespace ipl\Html\Contract;
use ipl\Html\Attributes;
/**
* Options for decorators
*
* This trait is intended for use by the classes which implement {@see DecoratorOptionsInterface}.
*/
trait DecoratorOptions
{
/** @var ?Attributes Attributes of the decorator */
protected ?Attributes $attributes = null;
/**
* Get the attributes
*
* @return Attributes
*/
public function getAttributes(): Attributes
{
if ($this->attributes === null) {
$this->attributes = new Attributes();
$this->registerAttributeCallbacks($this->attributes);
}
return $this->attributes;
}
/**
* Register attribute callbacks
*
* Override this method in order to register attribute callbacks in concrete classes.
*
* @param Attributes $attributes
*/
abstract protected function registerAttributeCallbacks(Attributes $attributes): void;
}

View File

@ -0,0 +1,18 @@
<?php
namespace ipl\Html\Contract;
use ipl\Html\Attributes;
/**
* Interface for decorators that provide options
*/
interface DecoratorOptionsInterface
{
/**
* Get the attributes (decorator options)
*
* @return Attributes
*/
public function getAttributes(): Attributes;
}

View File

@ -0,0 +1,39 @@
<?php
namespace ipl\Html\Contract;
use ipl\Html\FormDecoration\DecoratorChain;
/**
* Interface for form elements that support default element decoration
*
* @phpstan-import-type decoratorsFormat from DecoratorChain
* @phpstan-type loaderPaths array<int, array{0: class-string, 1?: string}>
*/
interface DefaultFormElementDecoration
{
/**
* Set the default element decorators.
*
* The default decorators will be applied to all elements that do not have explicit decorators.
* The order of the decorators is important, as it determines the rendering order.
*
* Please see {@see DecoratorChain::addDecorators()} for the supported array formats.
*
* @param decoratorsFormat $decorators
*
* @return $this
*/
public function setDefaultElementDecorators(array $decorators): static;
/**
* Add custom element decorator loader paths for the elements
*
* Each entry must be an array with index 0: class namespace, index 1: class name suffix (optional).
*
* @param loaderPaths $loaderPaths
*
* @return $this
*/
public function addElementDecoratorLoaderPaths(array $loaderPaths): static;
}

87
vendor/ipl/html/src/Contract/Form.php vendored Normal file
View File

@ -0,0 +1,87 @@
<?php
namespace ipl\Html\Contract;
use Evenement\EventEmitterInterface;
use Psr\Http\Message\ServerRequestInterface;
interface Form extends EventEmitterInterface
{
/** @var string Event emitted when the form is associated with a request but is not sent */
public const ON_REQUEST = 'request';
/** @var string Event emitted when the form has been sent */
public const ON_SENT = 'sent';
/** @var string Event emitted when the form is validated */
public const ON_VALIDATE = 'validate';
/** @var string Event emitted when the form has been submitted */
public const ON_SUBMIT = 'success';
/** @var string Event emitted in case of an error */
public const ON_ERROR = 'error';
/**
* Get the Form submission URL
*
* @return ?string
*/
public function getAction();
/**
* Get the HTTP method the form accepts
*
* @return string
*/
public function getMethod();
/**
* Get the request associated with the form
*
* @return ?ServerRequestInterface
*/
public function getRequest();
/**
* Handle the given request
*
* The following events will be emitted:
* - {@see self::ON_REQUEST} when the form is associated with a request but is not sent
* - {@see self::ON_SENT} when the form has been sent
* - {@see self::ON_SUBMIT} when the form has been submitted
* - {@see self::ON_ERROR} in case of an error
*
* @param ServerRequestInterface $request
*
* @return $this
*/
public function handleRequest(ServerRequestInterface $request);
/**
* Get whether the form has been sent
*
* A form is considered sent if the request's method equals the form's method.
*
* @return bool
*/
public function hasBeenSent();
/**
* Get whether the form has been submitted
*
* A form is submitted when it has been sent and a submit button, if set, has been pressed.
*
* @return bool
*/
public function hasBeenSubmitted();
/**
* Check if the form is valid
*
* Emits the {@see self::ON_VALIDATE} event if the form has not been validated before.
*
* @return bool
*/
public function isValid();
}

View File

@ -0,0 +1,40 @@
<?php
namespace ipl\Html\Contract;
/**
* Representation of a form decorator
*/
interface FormDecoration
{
/**
* Decorate the given form
*
* A decorator can create HTML elements and apply attributes to the given form.
* Only the elements added to {@see DecorationResult} are rendered in the end.
*
* The element can be added to the {@see DecorationResult} using the following three methods:
* - {@see DecorationResult::append()} will add the element to the end of the result.
* - {@see DecorationResult::prepend()} will add the element to the beginning of the result.
* - {@see DecorationResult::wrap()} will wrap the result with the given element.
*
* **Reference implementation:**
*
*```
* public function decorateForm(DecorationResult $result, Form $form): void
* {
* if (! $form->hasChanges()) {
* return;
* }
*
* $result->prepend(new HtmlElement('p', null, Text::create('You have unsaved changes.')));
* }
* ```
*
* @param DecorationResult $result
* @param Form $form
*
* @return void
*/
public function decorateForm(DecorationResult $result, Form $form): void;
}

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