Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use Espo\Core\Field\Address\AddressBuilder;
/**
* An address value object. Immutable.
*/
class Address
{
public function __construct(
private ?string $country = null,
private ?string $state = null,
private ?string $city = null,
private ?string $street = null,
private ?string $postalCode = null
) {}
/**
* Whether has a street.
*/
public function hasStreet(): bool
{
return $this->street !== null;
}
/**
* Whether has a city.
*/
public function hasCity(): bool
{
return $this->city !== null;
}
/**
* Whether has a country.
*/
public function hasCountry(): bool
{
return $this->country !== null;
}
/**
* Whether has a state.
*/
public function hasState(): bool
{
return $this->state !== null;
}
/**
* Whether has a postal code.
*/
public function hasPostalCode(): bool
{
return $this->postalCode !== null;
}
/**
* Get a street.
*/
public function getStreet(): ?string
{
return $this->street;
}
/**
* Get a city.
*/
public function getCity(): ?string
{
return $this->city;
}
/**
* Get a country.
*/
public function getCountry(): ?string
{
return $this->country;
}
/**
* Get a state.
*/
public function getState(): ?string
{
return $this->state;
}
/**
* Get a postal code.
*/
public function getPostalCode(): ?string
{
return $this->postalCode;
}
/**
* Clone with a street.
*/
public function withStreet(?string $street): self
{
return self::createBuilder()
->clone($this)
->setStreet($street)
->build();
}
/**
* Clone with a city.
*/
public function withCity(?string $city): self
{
return self::createBuilder()
->clone($this)
->setCity($city)
->build();
}
/**
* Clone with a country.
*/
public function withCountry(?string $country): self
{
return self::createBuilder()
->clone($this)
->setCountry($country)
->build();
}
/**
* Clone with a state.
*/
public function withState(?string $state): self
{
return self::createBuilder()
->clone($this)
->setState($state)
->build();
}
/**
* Clone with a postal code.
*/
public function withPostalCode(?string $postalCode): self
{
return self::createBuilder()
->clone($this)
->setPostalCode($postalCode)
->build();
}
/**
* Create an empty address.
*/
public static function create(): self
{
return new self();
}
/**
* Create a builder.
*/
public static function createBuilder(): AddressBuilder
{
return new AddressBuilder();
}
}

View File

@@ -0,0 +1,69 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Address;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\Address;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<Address>
*/
class AddressAttributeExtractor implements AttributeExtractor
{
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof Address) {
throw new InvalidArgumentException();
}
return (object) [
$field . 'Street' => $value->getStreet(),
$field . 'City' => $value->getCity(),
$field . 'Country' => $value->getCountry(),
$field . 'State' => $value->getState(),
$field . 'PostalCode' => $value->getPostalCode(),
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field . 'Street' => null,
$field . 'City' => null,
$field . 'Country' => null,
$field . 'State' => null,
$field . 'PostalCode' => null,
];
}
}

View File

@@ -0,0 +1,105 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Address;
use Espo\Core\Field\Address;
/**
* An address value builder.
*/
class AddressBuilder
{
private ?string $street;
private ?string $city;
private ?string $country;
private ?string $state;
private ?string $postalCode;
public function clone(Address $address): self
{
$this->setStreet($address->getStreet());
$this->setCity($address->getCity());
$this->setCountry($address->getCountry());
$this->setState($address->getState());
$this->setPostalCode($address->getPostalCode());
return $this;
}
public function setStreet(?string $street): self
{
$this->street = $street;
return $this;
}
public function setCity(?string $city): self
{
$this->city = $city;
return $this;
}
public function setCountry(?string $country): self
{
$this->country = $country;
return $this;
}
public function setState(?string $state): self
{
$this->state = $state;
return $this;
}
public function setPostalCode(?string $postalCode): self
{
$this->postalCode = $postalCode;
return $this;
}
public function build(): Address
{
return new Address(
$this->country,
$this->state,
$this->city,
$this->street,
$this->postalCode
);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Address;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\Address;
class AddressFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return true;
}
public function createFromEntity(Entity $entity, string $field): Address
{
return (new AddressBuilder())
->setStreet($entity->get($field . 'Street'))
->setCity($entity->get($field . 'City'))
->setCountry($entity->get($field . 'Country'))
->setState($entity->get($field . 'State'))
->setPostalCode($entity->get($field . 'PostalCode'))
->build();
}
}

View File

@@ -0,0 +1,40 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Address;
use Espo\Core\Field\Address;
/**
* An address formatter.
*/
interface AddressFormatter
{
public function format(Address $address): string;
}

View File

@@ -0,0 +1,63 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Address;
use RuntimeException;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Config;
class AddressFormatterFactory
{
public function __construct(
private AddressFormatterMetadataProvider $metadataProvider,
private InjectableFactory $injectableFactory,
private Config $config
) {}
public function create(int $format): AddressFormatter
{
/** @var ?class-string<AddressFormatter> $className */
$className = $this->metadataProvider->getFormatterClassName($format);
if (!$className) {
throw new RuntimeException("Unknown address format '{$format}'.");
}
return $this->injectableFactory->create($className);
}
public function createDefault(): AddressFormatter
{
$format = $this->config->get('addressFormat') ?? 1;
return $this->create($format);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Address;
use Espo\Core\Utils\Metadata;
class AddressFormatterMetadataProvider
{
private $metadata;
public function __construct(Metadata $metadata)
{
$this->metadata = $metadata;
}
public function getFormatterClassName(int $format): ?string
{
return $this->metadata->get([
'app', 'addressFormats', strval($format), 'formatterClassName',
]);
}
}

View File

@@ -0,0 +1,208 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use Espo\Core\Currency\CalculatorUtil;
use RuntimeException;
use InvalidArgumentException;
/**
* A currency value object. Immutable.
*/
class Currency
{
/** @var numeric-string */
private string $amount;
private string $code;
/**
* @param numeric-string|float|int $amount An amount.
* @param string $code A currency code.
* @throws RuntimeException
*/
public function __construct($amount, string $code)
{
if (!is_string($amount) && !is_float($amount) && !is_int($amount)) {
throw new InvalidArgumentException();
}
if (strlen($code) !== 3) {
throw new RuntimeException("Bad currency code.");
}
if (is_float($amount) || is_int($amount)) {
$amount = (string) $amount;
}
$this->amount = $amount;
$this->code = $code;
}
/**
* Get an amount as string.
*
* @return numeric-string
*/
public function getAmountAsString(): string
{
return $this->amount;
}
/**
* Get an amount.
*/
public function getAmount(): float
{
return (float) $this->amount;
}
/**
* Get a currency code.
*/
public function getCode(): string
{
return $this->code;
}
/**
* Add a currency value.
*
* @throws RuntimeException If currency codes are different.
*/
public function add(self $value): self
{
if ($this->getCode() !== $value->getCode()) {
throw new RuntimeException("Can't add a currency value with a different code.");
}
$amount = CalculatorUtil::add(
$this->getAmountAsString(),
$value->getAmountAsString()
);
return new self($amount, $this->getCode());
}
/**
* Subtract a currency value.
*
* @throws RuntimeException If currency codes are different.
*/
public function subtract(self $value): self
{
if ($this->getCode() !== $value->getCode()) {
throw new RuntimeException("Can't subtract a currency value with a different code.");
}
$amount = CalculatorUtil::subtract(
$this->getAmountAsString(),
$value->getAmountAsString()
);
return new self($amount, $this->getCode());
}
/**
* Multiply by a multiplier.
*/
public function multiply(float|int $multiplier): self
{
$amount = CalculatorUtil::multiply(
$this->getAmountAsString(),
(string) $multiplier
);
return new self($amount, $this->getCode());
}
/**
* Divide by a divider.
*/
public function divide(float|int $divider): self
{
$amount = CalculatorUtil::divide(
$this->getAmountAsString(),
(string) $divider
);
return new self($amount, $this->getCode());
}
/**
* Round with a precision.
*/
public function round(int $precision = 0): self
{
$amount = CalculatorUtil::round($this->getAmountAsString(), $precision);
return new self($amount, $this->getCode());
}
/**
* Compare with another currency value. Returns:
* - `1` if greater than the value;
* - `0` if equal to the value;
* - `-1` if less than the value.
*
* @throws RuntimeException If currency codes are different.
*/
public function compare(self $value): int
{
if ($this->getCode() !== $value->getCode()) {
throw new RuntimeException("Can't compare currencies with different codes.");
}
return CalculatorUtil::compare(
$this->getAmountAsString(),
$value->getAmountAsString()
);
}
/**
* Check whether the value is negative.
*/
public function isNegative(): bool
{
return $this->compare(self::create(0.0, $this->code)) === -1;
}
/**
* Create from an amount and code.
*
* @param numeric-string|float|int $amount An amount.
* @param string $code A currency code.
* @throws RuntimeException
*/
public static function create($amount, string $code): self
{
return new self($amount, $code);
}
}

View File

@@ -0,0 +1,79 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Currency;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\Currency;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<Currency>
*/
class CurrencyAttributeExtractor implements AttributeExtractor
{
public function __construct(
private string $entityType,
private Defs $ormDefs
) {}
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof Currency) {
throw new InvalidArgumentException();
}
$useString = $this->ormDefs
->getEntity($this->entityType)
->getField($field)
->getType() === Entity::VARCHAR;
$amount = $useString ?
$value->getAmountAsString() :
$value->getAmount();
return (object) [
$field => $amount,
$field . 'Currency' => $value->getCode(),
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field => null,
$field . 'Currency' => null,
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Currency;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\Currency;
use RuntimeException;
class CurrencyFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return $entity->get($field) !== null && $entity->get($field . 'Currency') !== null;
}
public function createFromEntity(Entity $entity, string $field): Currency
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
return new Currency(
$entity->get($field),
$entity->get($field . 'Currency')
);
}
}

View File

@@ -0,0 +1,304 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use Espo\Core\Field\DateTime\DateTimeable;
use DateTimeImmutable;
use DateTimeInterface;
use DateInterval;
use DateTimeZone;
use RuntimeException;
/**
* A date value object. Immutable.
*/
class Date implements DateTimeable
{
private string $value;
private DateTimeImmutable $dateTime;
private const SYSTEM_FORMAT = 'Y-m-d';
public function __construct(string $value)
{
if (!$value) {
throw new RuntimeException("Empty value.");
}
$this->value = $value;
$parsedValue = DateTimeImmutable::createFromFormat(
'!' . self::SYSTEM_FORMAT,
$value,
new DateTimeZone('UTC')
);
if ($parsedValue === false) {
throw new RuntimeException("Bad value.");
}
$this->dateTime = $parsedValue;
if ($this->value !== $this->dateTime->format(self::SYSTEM_FORMAT)) {
throw new RuntimeException("Bad value.");
}
}
/**
* Get a string value in `Y-m-d` format.
*/
public function toString(): string
{
return $this->value;
}
/**
* Get DateTimeImmutable.
*/
public function toDateTime(): DateTimeImmutable
{
return $this->dateTime;
}
/**
* Get a timestamp.
*/
public function toTimestamp(): int
{
return $this->dateTime->getTimestamp();
}
/**
* Get a year.
*/
public function getYear(): int
{
return (int) $this->dateTime->format('Y');
}
/**
* Get a month.
*/
public function getMonth(): int
{
return (int) $this->dateTime->format('n');
}
/**
* Get a day (of month).
*/
public function getDay(): int
{
return (int) $this->dateTime->format('j');
}
/**
* Get a day of week. 0 (for Sunday) through 6 (for Saturday).
*/
public function getDayOfWeek(): int
{
return (int) $this->dateTime->format('w');
}
/**
* Clones and modifies.
*/
public function modify(string $modifier): self
{
/** @var DateTimeImmutable|false $dateTime */
$dateTime = $this->dateTime->modify($modifier);
if (!$dateTime) {
throw new RuntimeException("Modify failure.");
}
return self::fromDateTime($dateTime);
}
/**
* Clones and adds an interval.
*/
public function add(DateInterval $interval): self
{
$dateTime = $this->dateTime->add($interval);
return self::fromDateTime($dateTime);
}
/**
* Clones and subtracts an interval.
*/
public function subtract(DateInterval $interval): self
{
$dateTime = $this->dateTime->sub($interval);
return self::fromDateTime($dateTime);
}
/**
* Add days.
*/
public function addDays(int $days): self
{
$modifier = ($days >= 0 ? '+' : '-') . abs($days) . ' days';
return $this->modify($modifier);
}
/**
* Add months.
*/
public function addMonths(int $months): self
{
$modifier = ($months >= 0 ? '+' : '-') . abs($months) . ' months';
return $this->modify($modifier);
}
/**
* Add years.
*/
public function addYears(int $years): self
{
$modifier = ($years >= 0 ? '+' : '-') . abs($years) . ' years';
return $this->modify($modifier);
}
/**
* A difference between another object (date or date-time) and self.
*/
public function diff(DateTimeable $other): DateInterval
{
return $this->toDateTime()->diff($other->toDateTime());
}
/**
* Whether greater than a given value.
*/
public function isGreaterThan(DateTimeable $other): bool
{
return $this->toDateTime() > $other->toDateTime();
}
/**
* Whether less than a given value.
*/
public function isLessThan(DateTimeable $other): bool
{
return $this->toDateTime() < $other->toDateTime();
}
/**
* Whether equals to a given value.
*/
public function isEqualTo(DateTimeable $other): bool
{
return $this->toDateTime() == $other->toDateTime();
}
/**
* Whether less than or equals to a given value.
* @since 9.0.0
*/
public function isLessThanOrEqualTo(DateTimeable $other): bool
{
return $this->isLessThan($other) || $this->isEqualTo($other);
}
/**
* Whether greater than or equals to a given value.
* @since 9.0.0
*/
public function isGreaterThanOrEqualTo(DateTimeable $other): bool
{
return $this->isGreaterThan($other) || $this->isEqualTo($other);
}
/**
* Create a today.
*/
public static function createToday(?DateTimeZone $timezone = null): self
{
$now = new DateTimeImmutable();
if ($timezone) {
$now = $now->setTimezone($timezone);
}
return self::fromDateTime($now);
}
/**
* Create from a string with a date in `Y-m-d` format.
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* Create from a DateTimeInterface.
*/
public static function fromDateTime(DateTimeInterface $dateTime): self
{
$value = $dateTime->format(self::SYSTEM_FORMAT);
return new self($value);
}
/**
* @deprecated As of v8.1. Use `toString` instead.
* @todo Remove in v10.0.
*/
public function getString(): string
{
return $this->toString();
}
/**
* @deprecated As of v8.1. Use `toDateTime` instead.
* @todo Remove in v10.0.
*/
public function getDateTime(): DateTimeImmutable
{
return $this->toDateTime();
}
/**
* @deprecated As of v8.1. Use `toTimestamp` instead.
* @todo Remove in v10.0.
*/
public function getTimestamp(): int
{
return $this->toTimestamp();
}
}

View File

@@ -0,0 +1,64 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Date;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\Date;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<Date>
*/
class DateAttributeExtractor implements AttributeExtractor
{
/**
* @param Date $value
*/
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof Date) {
throw new InvalidArgumentException();
}
return (object) [
$field => $value->toString(),
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field => null,
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Date;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\Date;
use RuntimeException;
class DateFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return $entity->get($field) !== null;
}
public function createFromEntity(Entity $entity, string $field): Date
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
return new Date(
$entity->get($field)
);
}
}

View File

@@ -0,0 +1,411 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use Espo\Core\Field\DateTime\DateTimeable;
use DateTimeImmutable;
use DateTimeInterface;
use DateInterval;
use DateTimeZone;
use RuntimeException;
/**
* A date-time value object. Immutable.
*/
class DateTime implements DateTimeable
{
private string $value;
private DateTimeImmutable $dateTime;
private const SYSTEM_FORMAT = 'Y-m-d H:i:s';
public function __construct(string $value)
{
if (!$value) {
throw new RuntimeException("Empty value.");
}
$normValue = strlen($value) === 16 ? $value . ':00' : $value;
$this->value = $normValue;
$parsedValue = DateTimeImmutable::createFromFormat(
self::SYSTEM_FORMAT,
$normValue,
new DateTimeZone('UTC')
);
if ($parsedValue === false) {
throw new RuntimeException("Bad value.");
}
$this->dateTime = $parsedValue;
if ($this->value !== $this->dateTime->format(self::SYSTEM_FORMAT)) {
throw new RuntimeException("Bad value.");
}
}
/**
* Get a string value in `Y-m-d H:i:s` format.
*/
public function toString(): string
{
return $this->value;
}
/**
* Get DateTimeImmutable.
*/
public function toDateTime(): DateTimeImmutable
{
return $this->dateTime;
}
/**
* Get a timestamp.
*/
public function toTimestamp(): int
{
return $this->dateTime->getTimestamp();
}
/**
* Get a year.
*/
public function getYear(): int
{
return (int) $this->dateTime->format('Y');
}
/**
* Get a month.
*/
public function getMonth(): int
{
return (int) $this->dateTime->format('n');
}
/**
* Get a day (of month).
*/
public function getDay(): int
{
return (int) $this->dateTime->format('j');
}
/**
* Get a day of week. 0 (for Sunday) through 6 (for Saturday).
*/
public function getDayOfWeek(): int
{
return (int) $this->dateTime->format('w');
}
/**
* Get a hour.
*/
public function getHour(): int
{
return (int) $this->dateTime->format('G');
}
/**
* Get a minute.
*/
public function getMinute(): int
{
return (int) $this->dateTime->format('i');
}
/**
* Get a second.
*/
public function getSecond(): int
{
return (int) $this->dateTime->format('s');
}
/**
* Get a timezone.
*/
public function getTimezone(): DateTimeZone
{
return $this->dateTime->getTimezone();
}
/**
* Clones and modifies.
*/
public function modify(string $modifier): self
{
/**
* @var DateTimeImmutable|false $dateTime
*/
$dateTime = $this->dateTime->modify($modifier);
if (!$dateTime) {
throw new RuntimeException("Modify failure.");
}
return self::fromDateTime($dateTime);
}
/**
* Clones and adds an interval.
*/
public function add(DateInterval $interval): self
{
$dateTime = $this->dateTime->add($interval);
return self::fromDateTime($dateTime);
}
/**
* Clones and subtracts an interval.
*/
public function subtract(DateInterval $interval): self
{
$dateTime = $this->dateTime->sub($interval);
return self::fromDateTime($dateTime);
}
/**
* Add days.
*/
public function addDays(int $days): self
{
$modifier = ($days >= 0 ? '+' : '-') . abs($days) . ' days';
return $this->modify($modifier);
}
/**
* Add months.
*/
public function addMonths(int $months): self
{
$modifier = ($months >= 0 ? '+' : '-') . abs($months) . ' months';
return $this->modify($modifier);
}
/**
* Add years.
*/
public function addYears(int $years): self
{
$modifier = ($years >= 0 ? '+' : '-') . abs($years) . ' years';
return $this->modify($modifier);
}
/**
* Add hours.
*/
public function addHours(int $hours): self
{
$modifier = ($hours >= 0 ? '+' : '-') . abs($hours) . ' hours';
return $this->modify($modifier);
}
/**
* Add minutes.
*/
public function addMinutes(int $minutes): self
{
$modifier = ($minutes >= 0 ? '+' : '-') . abs($minutes) . ' minutes';
return $this->modify($modifier);
}
/**
* Add seconds.
*/
public function addSeconds(int $seconds): self
{
$modifier = ($seconds >= 0 ? '+' : '-') . abs($seconds) . ' seconds';
return $this->modify($modifier);
}
/**
* A difference between another object (date or date-time) and self.
*/
public function diff(DateTimeable $other): DateInterval
{
return $this->toDateTime()->diff($other->toDateTime());
}
/**
* Clones and apply a timezone.
*/
public function withTimezone(DateTimeZone $timezone): self
{
$dateTime = $this->dateTime->setTimezone($timezone);
return self::fromDateTime($dateTime);
}
/**
* Clones and sets time. Null preserves a current value.
*/
public function withTime(?int $hour, ?int $minute, ?int $second = 0): self
{
$dateTime = $this->dateTime->setTime(
$hour ?? $this->getHour(),
$minute ?? $this->getMinute(),
$second ?? $this->getSecond()
);
return self::fromDateTime($dateTime);
}
/**
* Whether greater than a given value.
*/
public function isGreaterThan(DateTimeable $other): bool
{
return $this->toDateTime() > $other->toDateTime();
}
/**
* Whether less than a given value.
*/
public function isLessThan(DateTimeable $other): bool
{
return $this->toDateTime() < $other->toDateTime();
}
/**
* Whether equals to a given value.
*/
public function isEqualTo(DateTimeable $other): bool
{
return $this->toDateTime() == $other->toDateTime();
}
/**
* Whether less than or equals to a given value.
* @since 9.0.0
*/
public function isLessThanOrEqualTo(DateTimeable $other): bool
{
return $this->isLessThan($other) || $this->isEqualTo($other);
}
/**
* Whether greater than or equals to a given value.
* @since 9.0.0
*/
public function isGreaterThanOrEqualTo(DateTimeable $other): bool
{
return $this->isGreaterThan($other) || $this->isEqualTo($other);
}
/**
* Create a current time.
*/
public static function createNow(): self
{
return self::fromDateTime(new DateTimeImmutable());
}
/**
* Create from a string with a date-time in `Y-m-d H:i:s` format.
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* Create from a timestamp.
*/
public static function fromTimestamp(int $timestamp): self
{
$dateTime = (new DateTimeImmutable)->setTimestamp($timestamp);
return self::fromDateTime($dateTime);
}
/**
* Create from a DateTimeInterface.
*/
public static function fromDateTime(DateTimeInterface $dateTime): self
{
/** @var DateTimeImmutable $value */
$value = DateTimeImmutable::createFromFormat(
self::SYSTEM_FORMAT,
$dateTime->format(self::SYSTEM_FORMAT),
$dateTime->getTimezone()
);
$utcValue = $value
->setTimezone(new DateTimeZone('UTC'))
->format(self::SYSTEM_FORMAT);
$obj = new self($utcValue);
$obj->dateTime = $obj->dateTime->setTimezone($dateTime->getTimezone());
return $obj;
}
/**
* @deprecated As of v8.1. Use `toString` instead.
* @todo Remove in v10.0.
*/
public function getString(): string
{
return $this->toString();
}
/**
* @deprecated As of v8.1. Use `toDateTime` instead.
* @todo Remove in v10.0.
*/
public function getDateTime(): DateTimeImmutable
{
return $this->toDateTime();
}
/**
* @deprecated As of v8.1. Use `toTimestamp` instead.
* @todo Remove in v10.0.
*/
public function getTimestamp(): int
{
return $this->toTimestamp();
}
}

View File

@@ -0,0 +1,64 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\DateTime;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\DateTime;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<DateTime>
*/
class DateTimeAttributeExtractor implements AttributeExtractor
{
/**
* @param DateTime $value
*/
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof DateTime) {
throw new InvalidArgumentException();
}
return (object) [
$field => $value->toString(),
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field => null,
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\DateTime;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\DateTime;
use RuntimeException;
class DateTimeFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return $entity->get($field) !== null;
}
public function createFromEntity(Entity $entity, string $field): DateTime
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
return new DateTime(
$entity->get($field)
);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\DateTime;
use DateTimeImmutable;
interface DateTimeable
{
public function toDateTime(): DateTimeImmutable;
}

View File

@@ -0,0 +1,528 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use Espo\Core\Field\DateTime\DateTimeable;
use DateTimeImmutable;
use DateTimeInterface;
use DateInterval;
use DateTimeZone;
use RuntimeException;
/**
* A date-time or date. Immutable.
*/
class DateTimeOptional implements DateTimeable
{
private ?DateTime $dateTimeValue = null;
private ?Date $dateValue = null;
private const SYSTEM_FORMAT = 'Y-m-d H:i:s';
private const SYSTEM_FORMAT_DATE = 'Y-m-d';
public function __construct(string $value)
{
if (self::isStringDateTime($value)) {
$this->dateTimeValue = new DateTime($value);
} else {
$this->dateValue = new Date($value);
}
}
/**
* Create from a string with a date-time in `Y-m-d H:i:s` format or date in `Y-m-d`.
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* Create from a string with a date-time in `Y-m-d H:i:s` format.
* @noinspection PhpUnused
*/
public static function fromDateTimeString(string $value): self
{
if (!self::isStringDateTime($value)) {
throw new RuntimeException("Bad value.");
}
return self::fromString($value);
}
/**
* Get a string value in `Y-m-d H:i:s` format.
*/
public function toString(): string
{
return $this->getActualValue()->toString();
}
/**
* Get DateTimeImmutable.
*/
public function toDateTime(): DateTimeImmutable
{
return $this->getActualValue()->toDateTime();
}
/**
* Get a timestamp.
*/
public function toTimestamp(): int
{
return $this->getActualValue()->toDateTime()->getTimestamp();
}
/**
* Get a year.
*/
public function getYear(): int
{
return $this->getActualValue()->getYear();
}
/**
* Get a month.
*/
public function getMonth(): int
{
return $this->getActualValue()->getMonth();
}
/**
* Get a day (of month).
*/
public function getDay(): int
{
return $this->getActualValue()->getDay();
}
/**
* Get a day of week. 0 (for Sunday) through 6 (for Saturday).
*/
public function getDayOfWeek(): int
{
return $this->getActualValue()->getDayOfWeek();
}
/**
* Get an hour.
*/
public function getHour(): int
{
if ($this->isAllDay()) {
return 0;
}
/** @var DateTime $value */
$value = $this->getActualValue();
return $value->getHour();
}
/**
* Get a minute.
*/
public function getMinute(): int
{
if ($this->isAllDay()) {
return 0;
}
/** @var DateTime $value */
$value = $this->getActualValue();
return $value->getMinute();
}
/**
* Get a second.
*/
public function getSecond(): int
{
if ($this->isAllDay()) {
return 0;
}
/** @var DateTime $value */
$value = $this->getActualValue();
return $value->getSecond();
}
/**
* Whether is all-day (no time part).
*/
public function isAllDay(): bool
{
return $this->dateValue !== null;
}
/**
* Get a timezone.
*/
public function getTimezone(): DateTimeZone
{
return $this->toDateTime()->getTimezone();
}
private function getActualValue(): Date|DateTime
{
/** @var Date|DateTime */
return $this->dateValue ?? $this->dateTimeValue;
}
/**
* Clones and apply a timezone. Non-all-day value is created.
*/
public function withTimezone(DateTimeZone $timezone): self
{
if ($this->isAllDay()) {
$dateTime = $this->getActualValue()->toDateTime()->setTimezone($timezone);
return self::fromDateTime($dateTime);
}
/** @var DateTime $value */
$value = $this->getActualValue();
$dateTime = $value->withTimezone($timezone)->toDateTime();
return self::fromDateTime($dateTime);
}
/**
* Clones and sets time. Null preserves a current value.
*/
public function withTime(?int $hour, ?int $minute, ?int $second = 0): self
{
if ($this->isAllDay()) {
$dateTime = DateTime::fromDateTime($this->getActualValue()->toDateTime())
->withTime($hour, $minute, $second);
return self::fromDateTime($dateTime->toDateTime());
}
/** @var DateTime $value */
$value = $this->getActualValue();
$dateTime = $value->withTime($hour, $minute, $second);
return self::fromDateTime($dateTime->toDateTime());
}
/**
* Clones and modifies.
*/
public function modify(string $modifier): self
{
if ($this->isAllDay()) {
assert($this->dateValue !== null);
return self::fromDateTimeAllDay(
$this->dateValue->modify($modifier)->toDateTime()
);
}
assert($this->dateTimeValue !== null);
return self::fromDateTime(
$this->dateTimeValue->modify($modifier)->toDateTime()
);
}
/**
* Clones and adds an interval.
*/
public function add(DateInterval $interval): self
{
if ($this->isAllDay()) {
assert($this->dateValue !== null);
return self::fromDateTimeAllDay(
$this->dateValue->add($interval)->toDateTime()
);
}
assert($this->dateTimeValue !== null);
return self::fromDateTime(
$this->dateTimeValue->add($interval)->toDateTime()
);
}
/**
* Clones and subtracts an interval.
*/
public function subtract(DateInterval $interval): self
{
if ($this->isAllDay()) {
assert($this->dateValue !== null);
return self::fromDateTimeAllDay(
$this->dateValue->subtract($interval)->toDateTime()
);
}
assert($this->dateTimeValue !== null);
return self::fromDateTime(
$this->dateTimeValue->subtract($interval)->toDateTime()
);
}
/**
* Add days.
*/
public function addDays(int $days): self
{
$modifier = ($days >= 0 ? '+' : '-') . abs($days) . ' days';
return $this->modify($modifier);
}
/**
* Add months.
*/
public function addMonths(int $months): self
{
$modifier = ($months >= 0 ? '+' : '-') . abs($months) . ' months';
return $this->modify($modifier);
}
/**
* Add years.
*/
public function addYears(int $years): self
{
$modifier = ($years >= 0 ? '+' : '-') . abs($years) . ' years';
return $this->modify($modifier);
}
/**
* Add hours.
*/
public function addHours(int $hours): self
{
$modifier = ($hours >= 0 ? '+' : '-') . abs($hours) . ' hours';
return $this->modify($modifier);
}
/**
* Add minutes.
*/
public function addMinutes(int $minutes): self
{
$modifier = ($minutes >= 0 ? '+' : '-') . abs($minutes) . ' minutes';
return $this->modify($modifier);
}
/**
* Add seconds.
*/
public function addSeconds(int $seconds): self
{
$modifier = ($seconds >= 0 ? '+' : '-') . abs($seconds) . ' seconds';
return $this->modify($modifier);
}
/**
* A difference between another object (date or date-time) and self.
*/
public function diff(DateTimeable $other): DateInterval
{
return $this->toDateTime()->diff($other->toDateTime());
}
/**
* Whether greater than a given value.
*/
public function isGreaterThan(DateTimeable $other): bool
{
return $this->toDateTime() > $other->toDateTime();
}
/**
* Whether less than a given value.
*/
public function isLessThan(DateTimeable $other): bool
{
return $this->toDateTime() < $other->toDateTime();
}
/**
* Whether equals to a given value.
*/
public function isEqualTo(DateTimeable $other): bool
{
return $this->toDateTime() == $other->toDateTime();
}
/**
* Whether less than or equals to a given value.
* @since 9.0.0
*/
public function isLessThanOrEqualTo(DateTimeable $other): bool
{
return $this->isLessThan($other) || $this->isEqualTo($other);
}
/**
* Whether greater than or equals to a given value.
* @since 9.0.0
*/
public function isGreaterThanOrEqualTo(DateTimeable $other): bool
{
return $this->isGreaterThan($other) || $this->isEqualTo($other);
}
/**
* Create a current time.
*/
public static function createNow(): self
{
return self::fromDateTime(new DateTimeImmutable());
}
/**
* Create a today.
*/
public static function createToday(?DateTimeZone $timezone = null): self
{
$now = new DateTimeImmutable();
if ($timezone) {
$now = $now->setTimezone($timezone);
}
return self::fromDateTimeAllDay($now);
}
/**
* Create from a string with a date in `Y-m-d` format.
* @noinspection PhpUnused
*/
public static function fromDateString(string $value): self
{
if (self::isStringDateTime($value)) {
throw new RuntimeException("Bad value.");
}
return self::fromString($value);
}
/**
* Create from a timestamp.
*/
public static function fromTimestamp(int $timestamp): self
{
$dateTime = (new DateTimeImmutable)->setTimestamp($timestamp);
return self::fromDateTime($dateTime);
}
/**
* Create from a DateTimeInterface.
*/
public static function fromDateTime(DateTimeInterface $dateTime): self
{
/** @var DateTimeImmutable $value */
$value = DateTimeImmutable::createFromFormat(
self::SYSTEM_FORMAT,
$dateTime->format(self::SYSTEM_FORMAT),
$dateTime->getTimezone()
);
$utcValue = $value
->setTimezone(new DateTimeZone('UTC'))
->format(self::SYSTEM_FORMAT);
$obj = self::fromString($utcValue);
assert($obj->dateTimeValue !== null);
$obj->dateTimeValue = $obj->dateTimeValue->withTimezone($dateTime->getTimezone());
return $obj;
}
/**
* Create all-day from a DateTimeInterface.
*/
public static function fromDateTimeAllDay(DateTimeInterface $dateTime): self
{
$value = $dateTime->format(self::SYSTEM_FORMAT_DATE);
return new self($value);
}
private static function isStringDateTime(string $value): bool
{
if (strlen($value) > 10) {
return true;
}
return false;
}
/**
* @deprecated As of v8.1. Use `toString` instead.
* @todo Remove in v10.0.
*/
public function getString(): string
{
return $this->toString();
}
/**
* @deprecated As of v8.1. Use `toDateTime` instead.
* @todo Remove in v10.0.
*/
public function getDateTime(): DateTimeImmutable
{
return $this->toDateTime();
}
/**
* @deprecated As of v8.1. Use `toTimestamp` instead.
* @todo Remove in v10.0.
*/
public function getTimestamp(): int
{
return $this->toTimestamp();
}
}

View File

@@ -0,0 +1,73 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\DateTimeOptional;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\DateTimeOptional;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<DateTimeOptional>
*/
class DateTimeOptionalAttributeExtractor implements AttributeExtractor
{
/**
* @param DateTimeOptional $value
*/
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof DateTimeOptional) {
throw new InvalidArgumentException();
}
if ($value->isAllDay()) {
return (object) [
$field . 'Date' => $value->toString(),
$field => null,
];
}
return (object) [
$field => $value->toString(),
$field . 'Date' => null,
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field => null,
$field . 'Date' => null,
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\DateTimeOptional;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\DateTimeOptional;
use RuntimeException;
class DateTimeOptionalFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return $entity->get($field) !== null || $entity->get($field . 'Date') !== null;
}
public function createFromEntity(Entity $entity, string $field): DateTimeOptional
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
$stringValue = $entity->get($field . 'Date') ?? $entity->get($field);
return new DateTimeOptional($stringValue);
}
}

View File

@@ -0,0 +1,147 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
use FILTER_VALIDATE_EMAIL;
/**
* An email address value. Immutable.
*/
class EmailAddress
{
private string $address;
private bool $isOptedOut = false;
private bool $isInvalid = false;
public function __construct(string $address)
{
if ($address === '') {
throw new RuntimeException("Empty email address.");
}
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException("Not valid email address '{$address}'.");
}
$this->address = $address;
}
/**
* Get an address.
*/
public function getAddress(): string
{
return $this->address;
}
/**
* Whether opted-out.
*/
public function isOptedOut(): bool
{
return $this->isOptedOut;
}
/**
* Whether invalid.
*/
public function isInvalid(): bool
{
return $this->isInvalid;
}
/**
* Clone set invalid.
*/
public function invalid(): self
{
$obj = $this->clone();
$obj->isInvalid = true;
return $obj;
}
/**
* Clone set not invalid.
*/
public function notInvalid(): self
{
$obj = $this->clone();
$obj->isInvalid = false;
return $obj;
}
/**
* Clone set opted-out.
*/
public function optedOut(): self
{
$obj = $this->clone();
$obj->isOptedOut = true;
return $obj;
}
/**
* Clone set not opted-out.
*/
public function notOptedOut(): self
{
$obj = $this->clone();
$obj->isOptedOut = false;
return $obj;
}
/**
* Create from an address.
*/
public static function create(string $address): self
{
return new self($address);
}
private function clone(): self
{
$obj = new self($this->address);
$obj->isInvalid = $this->isInvalid;
$obj->isOptedOut = $this->isOptedOut;
return $obj;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\EmailAddress;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\EmailAddressGroup;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<EmailAddressGroup>
*/
class EmailAddressGroupAttributeExtractor implements AttributeExtractor
{
/**
* @param EmailAddressGroup $group
*/
public function extract(object $group, string $field): stdClass
{
if (!$group instanceof EmailAddressGroup) {
throw new InvalidArgumentException();
}
$primaryAddress = $group->getPrimary() ? $group->getPrimary()->getAddress() : null;
$dataList = [];
foreach ($group->getList() as $emailAddress) {
$dataList[] = (object) [
'emailAddress' => $emailAddress->getAddress(),
'lower' => strtolower($emailAddress->getAddress()),
'primary' => $primaryAddress && $emailAddress->getAddress() === $primaryAddress,
'optOut' => $emailAddress->isOptedOut(),
'invalid' => $emailAddress->isInvalid(),
];
}
return (object) [
$field => $primaryAddress,
$field . 'Data' => $dataList,
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field => null,
$field . 'Data' => [],
];
}
}

View File

@@ -0,0 +1,158 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\EmailAddress;
use Espo\Core\ORM\Type\FieldType;
use Espo\Entities\EmailAddress as EmailAddressEntity;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Repositories\EmailAddress as Repository;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\EmailAddress;
use Espo\Core\Field\EmailAddressGroup;
use Espo\Core\Utils\Metadata;
use RuntimeException;
use stdClass;
/**
* An email address group factory.
*/
class EmailAddressGroupFactory implements ValueFactory
{
private Metadata $metadata;
private EntityManager $entityManager;
/**
* @todo Use OrmDefs instead of Metadata.
*/
public function __construct(Metadata $metadata, EntityManager $entityManager)
{
$this->metadata = $metadata;
$this->entityManager = $entityManager;
}
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
$type = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, FieldParam::TYPE]);
if ($type !== FieldType::EMAIL) {
return false;
}
return true;
}
public function createFromEntity(Entity $entity, string $field): EmailAddressGroup
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
$emailAddressList = [];
$primaryEmailAddress = null;
$dataList = null;
$dataAttribute = $field . 'Data';
if ($entity->has($dataAttribute)) {
$dataList = $this->sanitizeDataList(
$entity->get($dataAttribute)
);
}
if (!$dataList && $entity->has($field) && !$entity->get($field)) {
$dataList = [];
}
if (!$dataList) {
/** @var Repository $repository */
$repository = $this->entityManager->getRepository(EmailAddressEntity::ENTITY_TYPE);
$dataList = $repository->getEmailAddressData($entity);
}
foreach ($dataList as $item) {
$emailAddress = EmailAddress::create($item->emailAddress);
if ($item->optOut ?? false) {
$emailAddress = $emailAddress->optedOut();
}
if ($item->invalid ?? false) {
$emailAddress = $emailAddress->invalid();
}
if ($item->primary ?? false) {
$primaryEmailAddress = $emailAddress;
}
$emailAddressList[] = $emailAddress;
}
$group = EmailAddressGroup::create($emailAddressList);
if ($primaryEmailAddress) {
$group = $group->withPrimary($primaryEmailAddress);
}
return $group;
}
/**
* @param array<int, array<string, mixed>|stdClass> $dataList
* @return stdClass[]
*/
private function sanitizeDataList(array $dataList): array
{
$sanitizedDataList = [];
foreach ($dataList as $item) {
if (is_array($item)) {
$sanitizedDataList[] = (object) $item;
continue;
}
if (!is_object($item)) {
throw new RuntimeException("Bad data.");
}
$sanitizedDataList[] = $item;
}
return $sanitizedDataList;
}
}

View File

@@ -0,0 +1,298 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
/**
* An email address group. Contains a list of email addresses. One email address is set as primary.
* If not empty, then there always should be a primary address. Immutable.
*/
class EmailAddressGroup
{
/** @var EmailAddress[] */
private array $list = [];
private ?EmailAddress $primary = null;
/**
* @param EmailAddress[] $list
* @throws RuntimeException
*/
public function __construct(array $list = [])
{
foreach ($list as $item) {
$this->list[] = clone $item;
}
$this->validateList();
if (count($this->list) !== 0) {
$this->primary = $this->list[0];
}
}
public function __clone()
{
$newList = [];
foreach ($this->list as $item) {
$newList[] = clone $item;
}
$this->list = $newList;
if ($this->primary) {
$this->primary = clone $this->primary;
}
}
/**
* Get a primary address as a string. If no primary, then returns null,
*/
public function getPrimaryAddress(): ?string
{
$primary = $this->getPrimary();
if (!$primary) {
return null;
}
return $primary->getAddress();
}
/**
* Get a primary email address.
*/
public function getPrimary(): ?EmailAddress
{
if ($this->isEmpty()) {
return null;
}
return $this->primary;
}
/**
* Get a list of all email addresses.
*
* @return EmailAddress[]
*/
public function getList(): array
{
return $this->list;
}
/**
* Get a number of addresses.
*/
public function getCount(): int
{
return count($this->list);
}
/**
* Get a list of email addresses w/o a primary.
*
* @return EmailAddress[]
*/
public function getSecondaryList(): array
{
$list = [];
foreach ($this->list as $item) {
if ($item === $this->primary) {
continue;
}
$list[] = $item;
}
return $list;
}
/**
* Get a list of email addresses represented as strings.
*
* @return string[]
*/
public function getAddressList(): array
{
$list = [];
foreach ($this->list as $item) {
$list[] = $item->getAddress();
}
return $list;
}
/**
* Get an email address by address represented as a string.
*/
public function getByAddress(string $address): ?EmailAddress
{
$index = $this->searchAddressInList($address);
if ($index === null) {
return null;
}
return $this->list[$index];
}
/**
* Whether an address is in the list.
*/
public function hasAddress(string $address): bool
{
return in_array($address, $this->getAddressList());
}
/**
* Clone with another primary email address.
*/
public function withPrimary(EmailAddress $emailAddress): self
{
$list = $this->list;
$index = $this->searchAddressInList($emailAddress->getAddress());
if ($index !== null) {
unset($list[$index]);
$list = array_values($list);
}
$newList = array_merge([$emailAddress], $list);
return self::create($newList);
}
/**
* Clone with an added email address list.
*
* @param EmailAddress[] $list
*/
public function withAddedList(array $list): self
{
$newList = $this->list;
foreach ($list as $item) {
$index = $this->searchAddressInList($item->getAddress());
if ($index !== null) {
$newList[$index] = $item;
continue;
}
$newList[] = $item;
}
return self::create($newList);
}
/**
* Clone with an added email address.
*/
public function withAdded(EmailAddress $emailAddress): self
{
return $this->withAddedList([$emailAddress]);
}
/**
* Clone with removed email address.
*/
public function withRemoved(EmailAddress $emailAddress): self
{
return $this->withRemovedByAddress($emailAddress->getAddress());
}
/**
* Clone with removed email address passed by an address.
*/
public function withRemovedByAddress(string $address): self
{
$newList = $this->list;
$index = $this->searchAddressInList($address);
if ($index !== null) {
unset($newList[$index]);
$newList = array_values($newList);
}
return self::create($newList);
}
/**
* Create with an optional email address list. A first item will be set as primary.
*
* @param EmailAddress[] $list
*/
public static function create(array $list = []): self
{
return new self($list);
}
private function searchAddressInList(string $address): ?int
{
foreach ($this->getAddressList() as $i => $item) {
if ($item === $address) {
return $i;
}
}
return null;
}
private function validateList(): void
{
$addressList = [];
foreach ($this->list as $item) {
if (!$item instanceof EmailAddress) {
throw new RuntimeException("Bad item.");
}
if (in_array(strtolower($item->getAddress()), $addressList)) {
throw new RuntimeException("Address list contains a duplicate.");
}
$addressList[] = strtolower($item->getAddress());
}
}
private function isEmpty(): bool
{
return count($this->list) === 0;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
/**
* A link value object. Immutable.
*/
class Link
{
private string $id;
private ?string $name = null;
public function __construct(string $id)
{
if (!$id) {
throw new RuntimeException("Empty ID.");
}
$this->id = $id;
}
/**
* Get an ID.
*/
public function getId(): string
{
return $this->id;
}
/**
* Get a name.
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Clone with a name.
*/
public function withName(?string $name): self
{
$obj = new self($this->id);
$obj->name = $name;
return $obj;
}
/**
* Create from an ID.
*/
public static function create(string $id, ?string $name = null): self
{
return (new self($id))->withName($name);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Link;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\Link;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<Link>
*/
class LinkAttributeExtractor implements AttributeExtractor
{
/**
* @param Link $value
*/
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof Link) {
throw new InvalidArgumentException();
}
return (object) [
$field . 'Id' => $value->getId(),
$field . 'Name' => $value->getName(),
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field . 'Id' => null,
$field . 'Name' => null,
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\Link;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\Link;
use RuntimeException;
class LinkFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return $entity->get($field . 'Id') !== null;
}
public function createFromEntity(Entity $entity, string $field): Link
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
$id = $entity->get($field . 'Id');
$name = $entity->get($field . 'Name');
return Link
::create($id)
->withName($name);
}
}

View File

@@ -0,0 +1,238 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
/**
* A link-multiple value object. Immutable.
*/
class LinkMultiple
{
/**
* @param LinkMultipleItem[] $list
* @throws RuntimeException
*/
public function __construct(private array $list = [])
{
$this->validateList();
}
public function __clone()
{
$newList = [];
foreach ($this->list as $item) {
$newList[] = clone $item;
}
$this->list = $newList;
}
/**
* Whether contains a specific ID.
*/
public function hasId(string $id): bool
{
return $this->searchIdInList($id) !== null;
}
/**
* Get a list of IDs.
*
* @return string[]
*/
public function getIdList(): array
{
$idList = [];
foreach ($this->list as $item) {
$idList[] = $item->getId();
}
return $idList;
}
/**
* Get a list of items.
*
* @return LinkMultipleItem[]
*/
public function getList(): array
{
return $this->list;
}
/**
* Get a number of items.
*/
public function getCount(): int
{
return count($this->list);
}
/**
* Get item by ID.
*/
public function getById(string $id): ?LinkMultipleItem
{
foreach ($this->list as $item) {
if ($item->getId() === $id) {
return $item;
}
}
return null;
}
/**
* Clone with an added ID.
*/
public function withAddedId(string $id): self
{
return $this->withAdded(LinkMultipleItem::create($id));
}
/**
* Clone with an added IDs.
*
* @param string[] $idList IDs.
*/
public function withAddedIdList(array $idList): self
{
$obj = $this;
foreach ($idList as $id) {
$obj = $obj->withAddedId($id);
}
return $obj;
}
/**
* Clone with an added item.
*/
public function withAdded(LinkMultipleItem $item): self
{
return $this->withAddedList([$item]);
}
/**
* Clone with an added item list.
* .
* @param LinkMultipleItem[] $list
*
* @throws RuntimeException
*/
public function withAddedList(array $list): self
{
$newList = $this->list;
foreach ($list as $item) {
$index = $this->searchIdInList($item->getId());
if ($index !== null) {
$newList[$index] = $item;
continue;
}
$newList[] = $item;
}
return self::create($newList);
}
/**
* Clone with removed item.
*/
public function withRemoved(LinkMultipleItem $item): self
{
return $this->withRemovedById($item->getId());
}
/**
* Clone with removed item by ID.
*/
public function withRemovedById(string $id): self
{
$newList = $this->list;
$index = $this->searchIdInList($id);
if ($index !== null) {
unset($newList[$index]);
$newList = array_values($newList);
}
return self::create($newList);
}
/**
* Create with an optional item list.
*
* @param LinkMultipleItem[] $list
*
* @throws RuntimeException
*/
public static function create(array $list = []): self
{
return new self($list);
}
private function validateList(): void
{
$idList = [];
foreach ($this->list as $item) {
if (!$item instanceof LinkMultipleItem) {
throw new RuntimeException("Bad item.");
}
if (in_array($item->getId(), $idList)) {
throw new RuntimeException("List contains duplicates.");
}
$idList[] = strtolower($item->getId());
}
}
private function searchIdInList(string $id): ?int
{
foreach ($this->getIdList() as $i => $item) {
if ($item === $id) {
return $i;
}
}
return null;
}
}

View File

@@ -0,0 +1,85 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\LinkMultiple;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\LinkMultiple;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<LinkMultiple>
*/
class LinkMultipleAttributeExtractor implements AttributeExtractor
{
/**
* @param LinkMultiple $value
*/
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof LinkMultiple) {
throw new InvalidArgumentException();
}
$nameMap = (object) [];
$columnData = (object) [];
foreach ($value->getList() as $item) {
$id = $item->getId();
$nameMap->$id = $item->getName();
$columnItemData = (object) [];
foreach ($item->getColumnList() as $column) {
$columnItemData->$column = $item->getColumnValue($column);
}
$columnData->$id = $columnItemData;
}
return (object) [
$field . 'Ids' => $value->getIdList(),
$field . 'Names' => $nameMap,
$field . 'Columns' => $columnData,
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field . 'Ids' => [],
$field . 'Names' => (object) [],
$field . 'Columns' => (object) [],
];
}
}

View File

@@ -0,0 +1,179 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\LinkMultiple;
use Espo\Core\ORM\Type\FieldType;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\LinkMultiple;
use Espo\Core\Field\LinkMultipleItem;
use Espo\Core\ORM\Entity as CoreEntity;
use RuntimeException;
use InvalidArgumentException;
use stdClass;
class LinkMultipleFactory implements ValueFactory
{
public function __construct(private Defs $ormDefs, private EntityManager $entityManager)
{}
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
$entityType = $entity->getEntityType();
$entityDefs = $this->ormDefs->getEntity($entityType);
if (!$entityDefs->hasField($field)) {
return false;
}
return $entityDefs->getField($field)->getType() === FieldType::LINK_MULTIPLE;
}
public function createFromEntity(Entity $entity, string $field): LinkMultiple
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
if (!$entity instanceof CoreEntity) {
throw new InvalidArgumentException();
}
$itemList = [];
if (!$entity->has($field . 'Ids') && !$entity->isNew()) {
$this->loadLinkMultipleField($entity, $field);
}
$idList = $entity->getLinkMultipleIdList($field);
$nameMap = $entity->get($field . 'Names') ?? (object) [];
$columnData = null;
if ($entity->hasAttribute($field . 'Columns')) {
$columnData = $entity->get($field . 'Columns') ?
$entity->get($field . 'Columns') :
$this->loadColumnData($entity, $field);
}
foreach ($idList as $id) {
$item = LinkMultipleItem::create($id);
if ($columnData && property_exists($columnData, $id)) {
$item = $this->addColumnValues($item, $columnData->$id);
}
$name = $nameMap->$id ?? null;
if ($name !== null) {
$item = $item->withName($name);
}
$itemList[] = $item;
}
return new LinkMultiple($itemList);
}
private function loadLinkMultipleField(CoreEntity $entity, string $field): void
{
$entity->loadLinkMultipleField($field);
}
private function loadColumnData(Entity $entity, string $field): stdClass
{
if ($entity->isNew()) {
return (object) [];
}
$columnData = (object) [];
$select = [Attribute::ID];
$entityDefs = $this->ormDefs->getEntity($entity->getEntityType());
$columns = $entityDefs->getField($field)->getParam('columns') ?? [];
if (count($columns) === 0) {
return $columnData;
}
$foreignEntityType = $entityDefs->tryGetRelation($field)?->tryGetForeignEntityType();
if ($foreignEntityType) {
$foreignEntityDefs = $this->entityManager->getDefs()->getEntity($foreignEntityType);
foreach ($columns as $column => $attribute) {
if (!$foreignEntityDefs->hasAttribute($attribute)) {
// For backward compatibility. If foreign attributes defined in the field do not exist.
unset($columns[$column]);
}
}
}
foreach ($columns as $item) {
$select[] = $item;
}
$collection = $this->entityManager
->getRDBRepository($entity->getEntityType())
->getRelation($entity, $field)
->select($select)
->find();
foreach ($collection as $itemEntity) {
$id = $itemEntity->getId();
$columnData->$id = (object) [];
foreach ($columns as $column => $attribute) {
$columnData->$id->$column = $itemEntity->get($attribute);
}
}
return $columnData;
}
private function addColumnValues(LinkMultipleItem $item, stdClass $data): LinkMultipleItem
{
foreach (get_object_vars($data) as $column => $value) {
$item = $item->withColumnValue($column, $value);
}
return $item;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
/**
* A link-multiple item. Immutable.
*/
class LinkMultipleItem
{
private string $id;
private ?string $name = null;
/** @var array<string, mixed> */
private array $columnData = [];
/**
* @throws RuntimeException
*/
public function __construct(string $id)
{
if ($id === '') {
throw new RuntimeException("Empty ID.");
}
$this->id = $id;
}
/**
* Get an ID.
*/
public function getId(): string
{
return $this->id;
}
/**
* Get a name.
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Get a column value.
*
* @return mixed
*/
public function getColumnValue(string $column)
{
return $this->columnData[$column] ?? null;
}
/**
* Whether a column value is set.
*/
public function hasColumnValue(string $column): bool
{
return array_key_exists($column, $this->columnData);
}
/**
* Get a list of set columns.
*
* @return array<int, string>
*/
public function getColumnList(): array
{
return array_keys($this->columnData);
}
/**
* Clone with a name.
*/
public function withName(string $name): self
{
$obj = $this->clone();
$obj->name = $name;
return $obj;
}
/**
* Clone with a column value.
*
* @param mixed $value
*/
public function withColumnValue(string $column, $value): self
{
$obj = $this->clone();
$obj->columnData[$column] = $value;
return $obj;
}
/**
* Create.
*
* @throws RuntimeException
*/
public static function create(string $id, ?string $name = null): self
{
$obj = new self($id);
$obj->name = $name;
return $obj;
}
private function clone(): self
{
$obj = new self($this->id);
$obj->name = $this->name;
$obj->columnData = $this->columnData;
return $obj;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use Espo\ORM\Entity;
use RuntimeException;
/**
* A link-parent value object. Immutable.
*/
class LinkParent
{
private string $entityType;
private string $id;
private ?string $name = null;
public function __construct(string $entityType, string $id)
{
if (!$entityType) {
throw new RuntimeException("Empty entity type.");
}
if (!$id) {
throw new RuntimeException("Empty ID.");
}
$this->entityType = $entityType;
$this->id = $id;
}
/**
* Get an ID.
*/
public function getId(): string
{
return $this->id;
}
/**
* Get an entity type.
*/
public function getEntityType(): string
{
return $this->entityType;
}
/**
* Get a name.
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Clone with a name.
*/
public function withName(?string $name): self
{
$obj = new self($this->entityType, $this->id);
$obj->name = $name;
return $obj;
}
/**
* Create.
*/
public static function create(string $entityType, string $id): self
{
return new self($entityType, $id);
}
/**
* Create from an entity.
*/
public static function createFromEntity(Entity $entity): self
{
return new self($entity->getEntityType(), $entity->getId());
}
}

View File

@@ -0,0 +1,68 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\LinkParent;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\LinkParent;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<LinkParent>
*/
class LinkParentAttributeExtractor implements AttributeExtractor
{
/**
* @param LinkParent $value
*/
public function extract(object $value, string $field): stdClass
{
if (!$value instanceof LinkParent) {
throw new InvalidArgumentException();
}
return (object) [
$field . 'Id' => $value->getId(),
$field . 'Type' => $value->getEntityType(),
$field . 'Name' => $value->getName(),
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field . 'Id' => null,
$field . 'Type' => null,
$field . 'Name' => null,
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\LinkParent;
use Espo\ORM\Entity;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\LinkParent;
use RuntimeException;
class LinkParentFactory implements ValueFactory
{
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
return $entity->get($field . 'Id') !== null && $entity->get($field . 'Type') !== null;
}
public function createFromEntity(Entity $entity, string $field): LinkParent
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
$id = $entity->get($field . 'Id');
$entityType = $entity->get($field . 'Type');
return LinkParent
::create($entityType, $id)
->withName($entity->get($field . 'Name'));
}
}

View File

@@ -0,0 +1,171 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
/**
* A phone number value. Immutable.
*/
class PhoneNumber
{
private string $number;
private ?string $type = null;
private bool $isOptedOut = false;
private bool $isInvalid = false;
public function __construct(string $number)
{
if ($number === '') {
throw new RuntimeException("Empty phone number.");
}
$this->number = $number;
}
/**
* Get a type.
*/
public function getType(): ?string
{
return $this->type;
}
/**
* Get a number.
*/
public function getNumber(): string
{
return $this->number;
}
/**
* Whether opted-out.
*/
public function isOptedOut(): bool
{
return $this->isOptedOut;
}
/**
* Whether invalid.
*/
public function isInvalid(): bool
{
return $this->isInvalid;
}
/**
* Clone with a type.
*/
public function withType(string $type): self
{
$obj = $this->clone();
$obj->type = $type;
return $obj;
}
/**
* Clone set invalid.
*/
public function invalid(): self
{
$obj = $this->clone();
$obj->isInvalid = true;
return $obj;
}
/**
* Clone set not invalid.
*/
public function notInvalid(): self
{
$obj = $this->clone();
$obj->isInvalid = false;
return $obj;
}
/**
* Clone set opted-out.
*/
public function optedOut(): self
{
$obj = $this->clone();
$obj->isOptedOut = true;
return $obj;
}
/**
* Clone set not opted-out.
*/
public function notOptedOut(): self
{
$obj = $this->clone();
$obj->isOptedOut = false;
return $obj;
}
/**
* Create with a number.
*/
public static function create(string $number): self
{
return new self($number);
}
/**
* Create from a number and type.
*/
public static function createWithType(string $number, string $type): self
{
return self::create($number)->withType($type);
}
private function clone(): self
{
$obj = new self($this->number);
$obj->type = $this->type;
$obj->isInvalid = $this->isInvalid;
$obj->isOptedOut = $this->isOptedOut;
return $obj;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\PhoneNumber;
use Espo\ORM\Value\AttributeExtractor;
use Espo\Core\Field\PhoneNumberGroup;
use stdClass;
use InvalidArgumentException;
/**
* @implements AttributeExtractor<PhoneNumberGroup>
*/
class PhoneNumberGroupAttributeExtractor implements AttributeExtractor
{
/**
* @param PhoneNumberGroup $group
*/
public function extract(object $group, string $field): stdClass
{
if (!$group instanceof PhoneNumberGroup) {
throw new InvalidArgumentException();
}
$primaryNumber = $group->getPrimary() ? $group->getPrimary()->getNumber() : null;
$dataList = [];
foreach ($group->getList() as $phoneNumber) {
$dataList[] = (object) [
'phoneNumber' => $phoneNumber->getNumber(),
'type' => $phoneNumber->getType(),
'primary' => $primaryNumber && $phoneNumber->getNumber() === $primaryNumber,
'optOut' => $phoneNumber->isOptedOut(),
'invalid' => $phoneNumber->isInvalid(),
];
}
return (object) [
$field => $primaryNumber,
$field . 'Data' => $dataList,
];
}
public function extractFromNull(string $field): stdClass
{
return (object) [
$field => null,
$field . 'Data' => [],
];
}
}

View File

@@ -0,0 +1,162 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field\PhoneNumber;
use Espo\Core\ORM\Type\FieldType;
use Espo\Entities\PhoneNumber as PhoneNumberEntity;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Repositories\PhoneNumber as Repository;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Value\ValueFactory;
use Espo\Core\Field\PhoneNumber;
use Espo\Core\Field\PhoneNumberGroup;
use Espo\Core\Utils\Metadata;
use RuntimeException;
use stdClass;
/**
* A phone number group factory.
*/
class PhoneNumberGroupFactory implements ValueFactory
{
private Metadata $metadata;
private EntityManager $entityManager;
/**
* @todo Use OrmDefs instead of Metadata.
*/
public function __construct(Metadata $metadata, EntityManager $entityManager)
{
$this->metadata = $metadata;
$this->entityManager = $entityManager;
}
public function isCreatableFromEntity(Entity $entity, string $field): bool
{
$type = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, FieldParam::TYPE]);
if ($type !== FieldType::PHONE) {
return false;
}
return true;
}
public function createFromEntity(Entity $entity, string $field): PhoneNumberGroup
{
if (!$this->isCreatableFromEntity($entity, $field)) {
throw new RuntimeException();
}
$phoneNumberList = [];
$primaryPhoneNumber = null;
$dataList = null;
$dataAttribute = $field . 'Data';
if ($entity->has($dataAttribute)) {
$dataList = $this->sanitizeDataList(
$entity->get($dataAttribute)
);
}
if (!$dataList && $entity->has($field) && !$entity->get($field)) {
$dataList = [];
}
if (!$dataList) {
/** @var Repository $repository */
$repository = $this->entityManager->getRepository(PhoneNumberEntity::ENTITY_TYPE);
$dataList = $repository->getPhoneNumberData($entity);
}
foreach ($dataList as $item) {
$phoneNumber = PhoneNumber::create($item->phoneNumber);
if ($item->type ?? false) {
$phoneNumber = $phoneNumber->withType($item->type);
}
if ($item->optOut ?? false) {
$phoneNumber = $phoneNumber->optedOut();
}
if ($item->invalid ?? false) {
$phoneNumber = $phoneNumber->invalid();
}
if ($item->primary ?? false) {
$primaryPhoneNumber = $phoneNumber;
}
$phoneNumberList[] = $phoneNumber;
}
$group = PhoneNumberGroup::create($phoneNumberList);
if ($primaryPhoneNumber) {
$group = $group->withPrimary($primaryPhoneNumber);
}
return $group;
}
/**
* @param array<int, array<string, mixed>|stdClass> $dataList
* @return stdClass[]
*/
private function sanitizeDataList(array $dataList): array
{
$sanitizedDataList = [];
foreach ($dataList as $item) {
if (is_array($item)) {
$sanitizedDataList[] = (object) $item;
continue;
}
if (!is_object($item)) {
throw new RuntimeException("Bad data.");
}
$sanitizedDataList[] = $item;
}
return $sanitizedDataList;
}
}

View File

@@ -0,0 +1,299 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Field;
use RuntimeException;
/**
* A phone number group. Contains a list of phone numbers. One phone number is set as primary.
* If not empty, then there always should be a primary number. Immutable.
*/
class PhoneNumberGroup
{
/** @var PhoneNumber[] */
private $list = [];
private ?PhoneNumber $primary = null;
/**
* @param PhoneNumber[] $list
*
* @throws RuntimeException
*/
public function __construct(array $list = [])
{
foreach ($list as $item) {
$this->list[] = clone $item;
}
$this->validateList();
if (count($this->list) !== 0) {
$this->primary = $this->list[0];
}
}
public function __clone()
{
$newList = [];
foreach ($this->list as $item) {
$newList[] = clone $item;
}
$this->list = $newList;
if ($this->primary) {
$this->primary = clone $this->primary;
}
}
/**
* Get a primary number as a string. If no primary, then returns null,
*/
public function getPrimaryNumber(): ?string
{
$primary = $this->getPrimary();
if (!$primary) {
return null;
}
return $primary->getNumber();
}
/**
* Get a primary phone number.
*/
public function getPrimary(): ?PhoneNumber
{
if ($this->isEmpty()) {
return null;
}
return $this->primary;
}
/**
* Get a list of all phone numbers.
*
* @return PhoneNumber[]
*/
public function getList(): array
{
return $this->list;
}
/**
* Get a number of phone numbers.
*/
public function getCount(): int
{
return count($this->list);
}
/**
* Get a list of phone numbers w/o a primary.
*
* @return PhoneNumber[]
*/
public function getSecondaryList(): array
{
$list = [];
foreach ($this->list as $item) {
if ($item === $this->primary) {
continue;
}
$list[] = $item;
}
return $list;
}
/**
* Get a list of phone numbers represented as strings.
*
* @return string[]
*/
public function getNumberList(): array
{
$list = [];
foreach ($this->list as $item) {
$list[] = $item->getNumber();
}
return $list;
}
/**
* Get a phone number by number represented as a string.
*/
public function getByNumber(string $number): ?PhoneNumber
{
$index = $this->searchNumberInList($number);
if ($index === null) {
return null;
}
return $this->list[$index];
}
/**
* Whether an number is in the list.
*/
public function hasNumber(string $number): bool
{
return in_array($number, $this->getNumberList());
}
/**
* Clone with another primary phone number.
*/
public function withPrimary(PhoneNumber $phoneNumber): self
{
$list = $this->list;
$index = $this->searchNumberInList($phoneNumber->getNumber());
if ($index !== null) {
unset($list[$index]);
$list = array_values($list);
}
$newList = array_merge([$phoneNumber], $list);
return self::create($newList);
}
/**
* Clone with an added phone number list.
*
* @param PhoneNumber[] $list
*/
public function withAddedList(array $list): self
{
$newList = $this->list;
foreach ($list as $item) {
$index = $this->searchNumberInList($item->getNumber());
if ($index !== null) {
$newList[$index] = $item;
continue;
}
$newList[] = $item;
}
return self::create($newList);
}
/**
* Clone with an added phone number.
*/
public function withAdded(PhoneNumber $phoneNumber): self
{
return $this->withAddedList([$phoneNumber]);
}
/**
* Clone with removed phone number.
*/
public function withRemoved(PhoneNumber $phoneNumber): self
{
return $this->withRemovedByNumber($phoneNumber->getNumber());
}
/**
* Clone with removed phone number passed by a number.
*/
public function withRemovedByNumber(string $number): self
{
$newList = $this->list;
$index = $this->searchNumberInList($number);
if ($index !== null) {
unset($newList[$index]);
$newList = array_values($newList);
}
return self::create($newList);
}
/**
* Create with an optional phone number list. A first item will be set as primary.
*
* @param PhoneNumber[] $list
*/
public static function create(array $list = []): self
{
return new self($list);
}
private function searchNumberInList(string $number): ?int
{
foreach ($this->getNumberList() as $i => $item) {
if ($item === $number) {
return $i;
}
}
return null;
}
private function validateList(): void
{
$numberList = [];
foreach ($this->list as $item) {
if (!$item instanceof PhoneNumber) {
throw new RuntimeException("Bad item.");
}
if (in_array($item->getNumber(), $numberList)) {
throw new RuntimeException("Number list contains a duplicate.");
}
$numberList[] = strtolower($item->getNumber());
}
}
private function isEmpty(): bool
{
return count($this->list) === 0;
}
}