Laravel: Объекты-Значения повсюду
Или можете прочитать подробную статью об основах.
Объект-Значение — элементарный класс, содержащий в основном (но не только) скалярные данные. Итак, этот класс-оболочка объединяющий связанную информацию. Вот пример:
class Percent
{
public readonly ?float $value;
public readonly string $formatted;
public function __construct(float $value)
{
$this->value = $value;
if ($value === null) {
$this->formatted = '';
} else {
$this->formatted = number_format($value * 100, 2) . '%';
}
}
public static function from(?float $value): self
{
return new self($value);
}
}
Этот класс представляет процентное значение. Этот простой класс даёт три преимущества:
- Инкапсулирует логику обрабатывающую значение
null
и представляет их в процентах. - У вас всегда есть два десятичных знака (по умолчанию) в ваших процентах.
- Улучшенные типы.
Важное примечание: бизнес-логика или расчёт не являются частью объекта-значения. Единственное исключение, которое я делаю, это базовое форматирование.
Вот и всё. Это объект-значение. Это объект содержащий некоторые значения. В исходном определении объекта-значения говорится ещё о двух вещах:
- Он иммутабельный. У вас нет сеттеров, только свойства только для чтения.
- Он не содержит
ID
или любого другого свойства связанного с идентификацией. Два объекта-значения равны только тогда, когда их значения одинаковы. Это основное различие между VO и DTO.
Моделирование Данных
Чтобы по настоящему понять объекты-значения, мы реализуем очень простое финансовое приложение. Что-то вроде Seekingalpha, Morningstar, Atom Finance, или Hypercharts. Если вы не знакомы с этими приложениями, вот упрощённое описание:
- В приложении мы храним компании. Публично торгуемые компании, такие как Apple или Microsoft.
- Также храним финансовые данные, такие как отчёты о прибылях и убытках.
- Приложение рассчитает некоторые важные показатели на основе этих данных. Например, норма прибыли, валовая прибыль и некоторые другие.
В примере приложения я реализую только некоторые метрики и буду хранить только отчёты о прибылях и убытках (без балансовых отчётов или финансовых потоков). Этого более чем достаточно для иллюстрации использования объектов-значений.
База данных будет выглядеть так:
Как видите, это довольно просто. Это пример строки из таблицы companies
:
id | ticker | name | price_per_share | market_cap |
---|---|---|---|---|
1 | AAPL | Apple Inc. | 14964 | 2420000 |
2 | MSFT | Microsoft Inc. | 27324 | 2040000 |
price_per_share
— текущая цена акций компании. Это значение хранится в центах, поэтому 14964
равно $149.64
. Это обычная практика позволяющая избежать ошибок округления.
market_cap
— текущая рыночная капитализация компании (price_per_share * количество акций). Значение хранится в миллионах, поэтому 2420000
это $2,420,000,000,000
или $2,420B
или $2.42T
. Хранение огромных финансовых чисел в миллионах (а в некоторых случаях в тысячах) также является обычной практикой в финансовых приложениях.
Теперь давайте рассмотрим таблицу income_statements
:
company_id | year | revenue | gross_profit |
---|---|---|---|
1 | 2022 | 386017 | 167231 |
1 | 2021 | 246807 | 167231 |
Каждая статья в отчёте о прибылях и убытках имеет свой собственный столбец, такой как revenue
или gross_profit
. Одна строка в таблице описывает год для данной компании. И, как вы, наверное, догадались, эти числа тоже исчисляются миллионами. Таким образом 386017
означает $386,017,000,000
или $386B
для краткости.
Если вам интересно, зачем хранить эти числа миллионами, ответ довольно прост: их легче читать. Просто посмотрите, например страницу Apple на Seekingalpha:
Таблица metrics
очень похожа на income_statements
:
company_id | year | gross_margin | profit_margin | pe_ratio |
---|---|---|---|---|
1 | 2022 | 0.43 | 0.26 | 2432 |
2 | 2022 | 0.68 | 0.34 | 2851 |
Каждая метрика имеет свой собственный столбец, и каждая строка представляет год для данной компании. Большинство метрик представляют собой процентные значения, хранящиеся в виде десятичных дробей. pe_ratio
означает price/earnings ratio
соотношение цена/прибыль. Если акции компании торгуются по цене 260 долларов, а её прибыли составляет 20 долларов на акцию, то соотношение P/E — 13,00. Это десятичное число, хранящееся как целое число.
Возможно вы спросите: Почему бы не назвать его
Это хороший вопрос! На мой взгляд, наша цель, как разработчиков программного обеспечения должна состоять в том, чтобы писать код, максимально приближённый к бизнес-языку. Но в финансовом секторе никто не называет это price_per_earnings_ratio
?отношением цены к прибыли
. Это просто соотношение PE
. Так что, на самом деле, это правильное название, на мой взгляд.
API
Мы хотим реализовать три API.
GET /companies/{company}
Возвращает базовый профиль компании:
{
"data": {
"id": 1,
"ticker": "AAPL",
"name": "Apple Inc.",
"price_per_share": {
"cent": 14964,
"dollar": 149.64,
"formatted": "$149.64"
},
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
}
}
Также возвращает данные о цене и рыночной капитализации ы удобочитаемом формате.
GET /companies/{company}/income-statements
Возвращает отчёт о прибыли и убытках, сгруппированные по статьям и годам:
{
"data": {
"years": [
2022,
2021
],
"revenue": {
"2022": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"2021": {
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
},
"eps": {
"2022": {
"cent": 620,
"dollar": 6.2,
"formatted": "$6.20"
},
"2021": {
"cent": 620,
"dollar": 6.2,
"formatted": "$6.20"
}
}
}
}
Правильная структура данных будет сильно зависеть от конкретного варианта использования и пользовательского интерфейса. Эта структура довольно хороша для макета, похожего на Seekingalpha (скриншот приводился ранее). Этот API также форматирует значения.
GET /companies/{company}/metrics
Это API возвращающий метрики:
{
"data": {
"years": [
2022
],
"gross_margin": {
"2022": {
"value": 0.43,
"formatted": "43.00%",
"top_line": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"bottom_line": {
"value": 167231000000,
"millions": 167231,
"formatted": "167,231"
}
}
},
"pe_ratio": {
"2022": {
"value": "24.32"
}
}
}
}
Каждое поле также содержит информацию top_line
и bottom_line
. В случае gross_margin
top_line
— доход, а bottom_line
— валовая прибыль.
Идентификация Объектов-Значений
Теперь, когда мы познакомились с базой данных и API, пришло время определить объекты-значения. Если вы внимательно посмотрите на JSON, сможете определить пять различных типов значений:
- Соотношение(Ratio). Простое число выраженное как число с плавающей запятой. На данный момент соотношение P/E — единственные данные этого типа в приложении.
- Прибыль(Margin). У него есть исходное значение, процент, значение верхней и нижней строки (
top_line
иbottom_line
). Валовая прибыль, операционная прибыль иprofit_margin
используют этот тип данных. - Цена(Price). У него есть цент, доллар и форматированное значение.
price_per_share
иeps
(прибыль на акцию) используют этот тип данных. - Рыночная капитализация(Market Cap). Уникален тем, что имеет три разных формата
2.42T
,242B
и577M
. Это всё допустимые числа для выражения рыночной капитализации компании. Когда компания достигает отметки в триллион, мы хотим использовать не1000B
, а1T
. Поэтому нам нужно обрабатывать эти случаи. - Миллионы(Millions). Каждая статья в отчёте о прибылях и убытках выражается в миллионах, поэтому имеет смысл использовать объект-значение называемый
Millions
.
Теперь взгляните на эти имена объектов-значений! Мы работаем над финансовым приложением, и у нас будут такие классы, как Millions
, Margin
или MarketCap
.
Это так кодовая база, которая имеет смысл даже спустя пять лет.
Реализация Объектов-Значений
Price
Price
кажется наиболее очевидным, поэтому давайте начнём с него. Сам класс довольно прост:
class Price
{
public readonly int $cent;
public readonly float $dollar;
public readonly string $formatted;
public function __construct(int $cent)
{
$this->cent = $cent;
$this->dollar = $cent / 100;
$this->formatted = '$' . number_format($this->dollar, 2);
}
public static function from(int $cent): self
{
return new self($cent);
}
}
Несколько важных вещей:
- Каждый объект-значение имеет
public readonly
свойства.readonly
гарантирует их неизменность, аpublic
упрощает доступ к ним, поэтому не нужно писать геттеры или сеттеры. - Многие объекты-значения имеют функцию-фабрику
from
. Она очень хорошо вписывается в общий стиль Laravel.
Этот объект можно использовать следующим образом:
$company = Company::first();
$price = Price::from($company->price_per_share);
Следующий вопрос: как использовать этот объект?
Есть два пути:
- Приведение значений на уровень Модели.
- Или приведение их на уровне API.
Приведение в модель
У нас есть как минимум два возможных решения для приведения атрибутов к объектам-значениям в моделях.
Использование аксессоров атрибутов:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Company extends Model
{
public function pricePerShare(): Attribute
{
return Attribute::make(
get: fn (int $value) => Price::from($value)
);
}
}
Это отличное решение, которое работает в 95% случаев. Однако сейчас мы находимся в оставшихся 5%, потому что у нас есть более 10 атрибутов, которые мы хотим привести. В модели IncomeStatement
нужно привести почти каждый атрибут к экземпляру Millions
. Просто представьте, как будет выглядеть класс с аксессорами атрибутов:
namespace App\Models;
class IncomeStatement extends Model
{
public function pricePerShare(): Attribute
{
return Attribute::make(
get: fn (int $value) => Millions::from($value)
);
}
/* подобный код здесь */
public function costOfRevenue(): Attribute {}
/* подобный код здесь */
public function grossProfit(): Attribute {}
/* подобный код здесь */
public function operatingExpenses(): Attribute {}
// ещё 8 методов здесь
}
Так что в нашем случае использование аксессоров не оптимально. К счастью, у Laravel есть решение! Можно извлечь логику приведения в отдельный класс Cast
:
namespace App\Models\Casts;
use App\ValueObjects\Price;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class PriceCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return Price::from($value);
}
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
Этот класс делает то же самое, что и аксессор атрибута:
get
вызывается при доступе к свойству из модели и преобразует целое число в объектPrice
.set
вызывается, когда вы устанавливаете свойство в модели перед её сохранением. Он должен преобразовать объектPrice
в целое число. Но, как видите, я просто оставил всё как есть, потому что нам это не нужно для примера. Если вы вернёте$value
из методаset
, Laravel не будет выполнять никакой дополнительной работы. Таким образом нет ни какой мутации атрибута.
Последним шагом является фактическое использование этого Cast
внутри модели Company
:
class Company extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'price_per_share' => PriceCast::class,
];
}
Теперь мы можем использовать его так:
$company = Company::first();
// После выполнения PriceCast::get()
$pricePerShare = $company->price_per_share;
// $127.89
echo $pricePerShare->formatted;
// 127.89
echo $pricePerShare->dollar;
// 12789
echo $pricePerShare->cent;
Где мы будем это использовать? Например, в ресурсах:
namespace App\Http\Resources;
class CompanyResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'ticker' => $this->ticker,
'name' => $this->name,
'price_per_share' => $this->price_per_share,
'market_cap' => $this->market_cap,
];
}
}
Поскольку эти объекты значений содержат только общедоступные свойства, Laravel автоматически преобразует их в массивы при преобразовании ответа в JSON. Таким образом, этот ресурс приведёт к следующему ответу JSON:
{
"data": {
"id": 1,
"ticker": "AAPL",
"name": "Apple Inc.",
"price_per_share": {
"cent": 14964,
"dollar": 149.64,
"formatted": "$149.64"
},
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
}
}
Так мы можем приводить значения в моделях Eloquent. Но мы можем пропустить эту настройку и привести значение непосредственно к ресурсам.
Приведение в ресурсы
Это намного проще, чем предыдущая часть. Всё, что нам нужно сделать, это создать объект Price
внутри ресурса:
namespace App\Http\Resources;
class CompanyResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'ticker' => $this->ticker,
'name' => $this->name,
'price_per_share' => Price::from($this->price_per_share),
'market_cap' => MarketCap::from($this->market_cap),
];
}
}
Теперь у модели Company
нет приведения (cast), поэтому мы просто создаём экземпляр объекта Price
и MarketCap
из целочисленных значений.
Как выбрать между ними?
- Честно говоря, трудно сказать без конкретного варианта использования.
- Однако если вам нужны только эти значения в API, то, возможно вы можете пропустить весь
Cast
и просто создать объект-значение в ресурсах. - Но если вам нужны эти значения для обработки других вариантов использования, более удобно использовать приведения Eloquent. Несколько примеров:
- Уведомления. Например, только что вышел новый отчёт о прибылях и убытках, и вы хотите уведомить об этом своих пользователей и включить в электронное письмо некоторые ключевые значения. Другим примером может быть уведомление о цене.
- Очередь заданий. Например, вам необходимо регулярно перечитывать метрики и значения, зависящие от цены.
- Трансляция через веб-сокет. Например, цена обновляется в режиме реального времени на FE.
- Каждый из этих сценариев может выиграть от использования Eloquent Cast, потому что в противном случае вы в конечном итоге создадите экземпляры этих объектов-значений в каждом месте.
- В общем, я считаю хорошей идеей использовать эти объекты в моделях. Это делает вашу кодовую базу более высокоуровневой и простой в обслуживании.
Поэтому я собираюсь использовать Eloquent Cast для управления приведением.
MarketCap
Как обсуждалось ранее, рыночная капитализация (market cap) немного более уникальна, поэтому у неё есть собственный объект стоимости. Нам нужна эта структура данных:
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
Свойство formatted
будет меняться в зависимости от рыночной капитализации компании, например:
"market_cap": {
"millions": 204100,
"formatted": "204.1B"
}
И последний случай:
"market_cap": {
"millions": 172,
"formatted": "172M"
}
Как класс, это выглядит так:
namespace App\ValueObjects;
class MarketCap
{
public readonly int $millions;
public readonly string $formatted;
public function __construct(int $millions)
{
$this->millions = $millions;
// Trillions
if ($millions >= 1_000_000) {
$this->formatted = number_format($this->millions / 1_000_000, 2) . 'T';
}
// Billions
if ($millions < 1_000_000 && $millions >= 1_000) {
$this->formatted = number_format($this->millions / 1_000, 1) . 'B';
}
// Millions
if ($millions < 1_000) {
$this->formatted = number_format($this->millions) . 'M';
}
}
public static function from(int $millions): self
{
return new self($millions);
}
}
Нужно проверить значение $millions
, выполнить соответствующее деление и использовать правильный суффикс.
Приведение почти идентично PriceCast
:
namespace App\Models\Casts;
class MarketCapCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return MarketCap::from($value);
}
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
Опять, не нужно ничего делать в set
. Последнее, что нужно использовать это приведение:
namespace App\Models;
class Company extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'price_per_share' => PriceCast::class,
'market_cap' => MarketCapCast::class,
];
}
Я не буду перечислять другие классы Cast, потому что они одинаковые. Вы можете посмотреть их в репозитории.
Millions
Этот объект-значение довольно прост:
namespace App\ValueObjects;
class Millions
{
public readonly int $value;
public readonly int $millions;
public readonly string $formatted;
public function __construct(int $millions)
{
$this->value = $millions * 1_000_000;
$this->millions = $millions;
$this->formatted = number_format($this->millions, 0, ',');
}
public static function from(int $millions): self
{
return new self($millions);
}
}
У него три свойства:
value
содержит необработанное число в виде целого числа.millions
содержит число, выраженное в миллионах.formatted
содержит отформатированное число, например192,557
Как JSON:
"revenue": {
"2022": {
"value": 192557000000,
"millions": 192557,
"formatted": "192,557"
}
}
Millions
использует модель IncomeStatement
, и именно здесь мы выигрываем от использования Eloquent Casts
:
namespace App\Models;
class IncomeStatement extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'revenue' => MillionsCast::class,
'cost_of_revenue' => MillionsCast::class,
'gross_profit' => MillionsCast::class,
'operating_expenses' => MillionsCast::class,
'operating_profit' => MillionsCast::class,
'interest_expense' => MillionsCast::class,
'income_tax_expense' => MillionsCast::class,
'net_income' => MillionsCast::class,
'eps' => PriceCast::class,
];
}
Margin
Это также довольно простой класс:
namespace App\ValueObjects;
class Margin
{
public readonly float $value;
public readonly string $formatted;
public readonly Millions $top_line;
public readonly Millions $bottom_line;
public function __construct(
float $value,
Millions $topLine,
Millions $bottomLine
) {
$this->value = $value;
$this->top_line = $topLine;
$this->bottom_line = $bottomLine;
$this->formatted = number_format($value * 100, 2) . '%';
}
public static function make(
float $value,
Millions $topLine,
Millions $bottomLine
): self {
return new self($value, $topLine, $bottomLine);
}
}
Он демонстрирует одну замечательную особенность объектов-значений: они могут быть вложены друг в друга. В этом примере атрибуты top_line
и bottom_line
экземпляры Millions
. Эти цифры описывают как рассчитывается прибыль. Например, валовая прибыль рассчитывается путём деления дохода (top_line
) на валовую прибыль (bottom_line
). В JSON это будет выглядеть так:
"gross_margin": {
"2022": {
"value": 0.68,
"formatted": "68.00%",
"top_line": {
"value": 192557000000,
"millions": 192557,
"formatted": "192,557"
},
"bottom_line": {
"value": 132345000000,
"millions": 132345,
"formatted": "132,345"
}
}
}
Однако если вы посмотрите на метод make
вы увидите, что мы ожидаем два дополнительных параметра: $topLine
и $bottomLine
. Значит, мы можем использовать этот объект следующим образом:
$company = Company::first();
$incomeStatement = $company->income_statements()
->where('year', 2022)
->first();
$metrics = $company->metrics()->where('year', 2022)->first();
$grossMargin = Margin::make(
$metrics->gross_margin,
$incomeStatement->revenue,
$incomeStatement->gross_profit,
);
Поскольку мы используем Eloquent Casts
, нам нужен revenue
и gross_profit
(в этом конкретном примере) в класс MarginCast
. Мы можем сделать что-то вроде этого:
namespace App\Models\Casts;
class MarginCast implements CastsAttributes
{
/**
* @param Metric $model
*/
public function get($model, $key, $value, $attributes)
{
$incomeStatement = $model
->company
->income_statements()
->where('year', $model->year)
->first();
[$topLine, $bottomLine] = $model->getTopAndBottomLine(
$incomeStatement,
$key,
);
return Margin::make($value, $topLine, $bottomLine);
}
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
Как видите, модель в данном случае, модель Metric
(именно здесь будет использоваться приведение), поэтому мы можем запросить соответствующий отчёт о прибылях и убытках за тот же год. После этого нам нужен метод, который может возвращать top_line
и bottom_line
для конкретной метрики.
namespace App\Models;
class Metric extends Model
{
public function getTopAndBottomLine(
IncomeStatement $incomeStatement,
string $metricName
): array {
return match ($metricName) {
'gross_margin' => [
$incomeStatement->revenue,
$incomeStatement->gross_profit
],
'operating_margin' => [
$incomeStatement->revenue,
$incomeStatement->operating_profit
],
'profit_margin' => [
$incomeStatement->revenue,
$incomeStatement->net_income
],
};
}
}
Этот метод просто возвращает правильные статьи из отчёта о прибылях и убытках. Логика довольно проста, но намного сложнее остальных, поэтому я рекомендую ознакомиться с исходным кодом и открыть эти классы.
Вы можете сказать: Подождите минутку… Мы запрашиваем компании и отчёты о прибылях и убытках в MarginCast по каждому атрибуту??? Это примерно 10 дополнительных запросов каждый раз, когда мы запрашиваем просу метрику, верно?
Хороший вопрос! Ответ: нет. Эти приведения выполняются лениво. Это означает, что функция get
будет выполняться только тогда, когда вы действительно получите доступ к данному свойству. Но, как вы уже догадались, мы будем обращаться к каждому свойству ресурса, поэтому будет выполнено множество дополнительных запросов. Что можно с этим сделать?
- Нетерпеливая загрузка отношений при запросе метрики. Это предотвратит возникновение проблемы с запросами N+1.
- Кэшируйте отчёты о доходах. Ведь это исторические данные, обновляемые раз в год. Это также предотвратит лишние запросы.
- Если производительность всё ещё остаётся проблемой, вы можете отказаться от всего класса
MarginCast
и напрямую использовать объект в ресурсе. В этом случае у вас больше гибкости. Например, вы можете запрашивать все важные данные в одном запросе и взаимодействовать только с коллекциями при определении верхних и нижних значений строки.
PeRatio
После всех сложностей давайте посмотрим последний и, наверное, самый простой Объект-Значение:
namespace App\ValueObjects;
class PeRatio
{
public readonly string $value;
public function __construct(int $peRatio)
{
$this->value = number_format($peRatio / 100, 2);
}
public static function from(int $peRatio): self
{
return new self($peRatio);
}
}
Этот класс также можно использовать для покрытия других чисел типа ratio
, но прямо сейчас PE является единственным, поэтому я решил назвать класс PeRatio
.
Сводный отчёт о прибылях и убытках
Теперь, когда у нас есть все объекты значений, мы можем перейти к ресурсу. Наша цель состоит в том, чтобы получить сводное представление о прибылях и убытках компании. Это структура JSON:
"data": {
"years": [
2022,
2021
],
"items": {
"revenue": {
"2022": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"2021": {
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
}
}
}
Есть два способа решить эту проблему:
- Более
статичный
подход. - И более
динамичный
.
По динамичным
я подразумеваю что-то вроде этого:
class IncomeStatementResource
{
public $preserveKeys = true;
public function toArray(Request $request)
{
$data = [];
// $this is a Company
$data['years'] = $this->income_statements->pluck('year');
foreach ($this->income_statements as $incomeStatement) {
foreach ($incomeStatement->getAttributes() as $attribute => $value) {
$notRelated = [
'id', 'year', 'company_id', 'created_at', 'updated_at',
];
if (in_array($attribute, $notRelated)) {
continue;
}
Arr::set(
$data,
"items.{$attribute}.{$incomeStatement->year}",
$incomeStatement->{$attribute}
);
}
}
return $data;
}
}
Трудно разобраться в происходящем? Это не ваша вина! Это моё. Этот код — отстой. Я имею в виду, что это очень динамично
, поэтому оно будет работать независимо от того, есть ли у вас четыре столбца в income_statements
или 15. Но кроме этого он кажется немного напуганным. Более того, он не имеет настоящей
формы, поэтому очень странно помещать его в ресурс.
Не поймите меня неправильно, иногда вам просто нужны такие решения. Но отчёт о прибылях и убытках имеет конечное количество статей (столбцов), и это не то, что подлежит изменению.
Давайте посмотрим более декларативный подход:
namespace App\Http\Resources;
class IncomeStatementsSummaryResource extends JsonResource
{
public $preserveKeys = true;
public function toArray($request)
{
// $this is a Collection<IncomeStatement>
$years = $this->pluck('year');
return [
'years' => $years,
'items' => [
'revenue' => $this->getItem('revenue', $years),
'cost_of_revenue' => $this->getItem('cost_of_revenue', $years),
'gross_profit' => $this->getItem('gross_profit', $years),
'operating_expenses' => $this->getItem('operating_expenses', $years),
'operating_profit' => $this->getItem('operating_profit', $years),
'interest_expense' => $this->getItem('interest_expense', $years),
'income_tax_expense' => $this->getItem('income_tax_expense', $years),
'net_income' => $this->getItem('net_income', $years),
'eps' => $this->getItem('eps', $years ),
]
];
}
/**
* @return array<int, int>
*/
private function getItem(string $name, Collection $years): array
{
$data = [];
foreach ($years as $year) {
$data[$year] = $this
->where('year', $year)
->first()
->{$name};
}
return $data;
}
}
Вы видите разницу? Он прост для понимания, читабелен, имеет реальную форму и вообще не требует дополнительного кода. Однако он называется IncomeStatementsSummaryResource
, и на, то есть причина. Для этого ресурса требуется Collection<IncomeStatement>
, поэтому его можно использовать следующим образом:
namespace App\Http\Controllers;
class IncomeStatementController extends Controller
{
public function index(Company $company)
{
return IncomeStatementsSummaryResource::make($company->income_statements);
}
}
Мы передаём все отчёты о прибылях и убытках компании как Collection
. Таким образом, эта строка в ресурсе не будет выполнять дополнительные запросы:
// $this->where() is a Collection method
$data[$year] = $this->where('year', $year)->first()->{$name};
Последняя важная вещь — вот эта строка:
public $preserveKeys = true;
Без этого Laravel переопределит ключи массива и преобразует года в стандартные индексы массива с отсчётом от нуля:
"data": {
"years": [
2022,
2021
],
"items": {
"revenue": [
{
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
{
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
]
}
}
Как видите, объект с указанием года становится массивом JSON. Вот почему я использовал свойство $preserveKeys
из родительского класса JsonResource
.
Сводка Метрик
API сводки метрик в основном такой же, как сводный отчёт о прибылях и убытках. Поэтому не удивительно, что Ресурс выглядит почти так же:
namespace App\Http\Resources;
class MetricsSummaryResource extends JsonResource
{
public $preserveKeys = true;
public function toArray($request)
{
$years = $this->pluck('year');
return [
'years' => $years,
'items' => [
'gross_margin' => $this->getItem('gross_margin', $years),
'operating_margin' => $this->getItem('operating_margin', $years),
'profit_margin' => $this->getItem('profit_margin', $years),
'pe_ratio' => $this->getItem('pe_ratio', $years),
]
];
}
private function getItem(string $name, Collection $years): array
{
$data = [];
foreach ($years as $year) {
$data[$year] = $this
->where('year', $year)
->first()
->{$name};
}
return $data;
}
}
Можно использовать так:
namespace App\Http\Controllers;
class MetricController extends Controller
{
public function index(Company $company)
{
return MetricsSummaryResource::make($company->metrics);
}
}
Заключение
Это был длинный эксклюзив, я знаю. Дайте ему немного времени, может быть, перечитаете его позже.
На мой взгляд, объекты-значения потрясающие! Я использую их почти в каждом проекте, независимо от того, он новый, DDD или не DDD, легаси или нет. Их довольно легко начать использовать, и у вас будет очень высокоуровневая декларативная кодовая база.
Мне часто задают вопрос: Что ещё можно выразить в виде объекта-значения?
Почти всё, назову несколько примеров:
- Адрес. В приложении электронной коммерции, где приходится иметь дело с доставкой, может быть полезно использовать объекты-значения вместо строк. Вы можете выразить каждую часть адреса как свойство:
- Страна
- Почтовый индекс
- 1 строка адреса
- 2 строка адреса
- Числа и проценты. Как мы видели.
- Адрес электронной почты.
- Имя. С такими частями, как имя, отчество и фамилия.
- Любые единицы измерения, такие как вес, температура, расстояние, координаты GPS.
EndDate
иStartDate
. Их можно создать из Carbon, но убедитесь, чтоStartDate
всегда равен00:00:00
, аEndDate
всегда равен23:59:59
.- Любые другие концепции, специфичные для приложения.