php数组和集合_在PHP中创建严格类型的数组和集合

php数组和集合

This post first appeared on Medium and was republished here with the author’s permission. We encourage you to follow Bert on Medium and give him some likes there!

这篇文章首先出现在Medium上 ,并在作者许可下在此处重新发布。 我们鼓励您关注Bert on Medium,并给他一些喜欢!



One of the language features announced back in PHP 5.6 was the addition of the ... token to denote that a function or method accepts a variable length of arguments.

PHP 5.6中宣布的语言功能之一是添加...令牌,以表示函数或方法接受可变长度的参数。

Something I rarely see mentioned is that it’s possible to combine this feature with type hints to essentially create typed arrays.

我很少看到提到的一点是,可以将此功能与类型提示结合使用以本质上创建类型化数组。

For example, we could have a Movie class with a method to set an array of air dates that only accepts DateTimeImmutable objects:

例如,我们可以使用一个Movie类,该类具有一种方法来设置一个仅接受DateTimeImmutable对象的广播日期数组:

<?php

class Movie {  
  private $dates = [];

  public function setAirDates(\DateTimeImmutable ...$dates) {
    $this->dates = $dates;
  }

  public function getAirDates() {
    return $this->dates;
  }
}

We can now pass a variable number of separate DateTimeImmutable objects to the setAirDates() method:

现在,我们可以将可变数量的单独的DateTimeImmutable对象传递给setAirDates()方法:

<?php

$movie = new Movie();

$movie->setAirDates(
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-01-28'),
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-02-22')
);

If we were to pass something else than a DateTimeImmutable, a string for example, a fatal error would be thrown:

如果我们要传递DateTimeImmutable以外的其他内容(例如字符串),则会引发致命错误:

Catchable fatal error: Argument 1 passed to Movie::setAirDates() must be an instance of DateTimeImmutable, string given.

If we instead already had an array of DateTimeImmutable objects that we wanted to pass to setAirDates(), we could again use the ... token, but this time to unpack them:

如果我们已经有了要传递给setAirDates()DateTimeImmutable对象数组,则可以再次使用...令牌,但这一次将其解压缩:

<?php

$dates = [
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-01-28'),
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-02-22'),
];

$movie = new Movie();
$movie->setAirDates(...$dates);

If the array were to contain a value that is not of the expected type, we would still get the fatal error mentioned earlier.

如果数组包含的值不是预期的类型,我们仍然会遇到前面提到的致命错误。

Additionally, we can use scalar types the same way starting from PHP 7. For example, we can add a method to set a list of ratings as floats on our Movie class:

此外,我们可以从PHP 7开始使用标量类型。例如,可以在Movie类中添加一个方法来将等级列表设置为浮点数:

<?php

declare(strict_types=1);

class Movie {
  private $dates = [];
  private $ratings = [];

  public function setAirDates(\DateTimeImmutable ...$dates) { /* ... */ }
  public function getAirDates() : array { /* ... */ }

  public function setRatings(float ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverageRating() : float {
    if (empty($this->ratings)) {
      return 0;
    }

    $total = 0;

    foreach ($this->ratings as $rating) {
      $total += $rating;
    }

    return $total / count($this->ratings);
  }
}

Again, this ensures that the ratings property will always contain floats without us having to loop over all the contents to validate them. So now we can easily do some math operations on them in getAverageRating(), without having to worry about invalid types.

再次,这确保了rating属性将始终包含浮点数,而无需我们遍历所有内容以对其进行验证。 因此,现在我们可以轻松地在getAverageRating()对它们进行一些数学运算,而不必担心无效类型。

这种类型数组的问题 (Problems with This Kind of Typed Arrays)

One of the downsides of using this feature as typed arrays is that we can only define one such array per method. Let’s say we wanted to have a Movie class that expects a list of air dates together with a list of ratings in the constructor, instead of setting them later via optional methods. This would be impossible with the method used above.

使用此功能作为类型化数组的缺点之一是,每个方法只能定义一个这样的数组。 假设我们希望有一个Movie类,该类期望广播日期列表以及构造函数中的收视率列表,而不是稍后通过可选方法进行设置。 使用上面使用的方法将是不可能的。

Another problem is that when using PHP 7, the return types of our get() methods would still have to be “array”, which is often too generic.

另一个问题是,在使用PHP 7时,我们的get()方法的返回类型仍然必须是“数组”,这通常太通用了。

解决方案:集合类 (Solution: Collection Classes)

To fix both problems, we can simply inject our typed arrays inside so-called “collection” classes. This also improves our separation of concerns, because we can now move the calculation method for the average rating to the relevant collection class:

为了解决这两个问题,我们可以简单地将类型化的数组注入所谓的“集合”类中。 这也改善了我们的关注点分离,因为我们现在可以将平均评级的计算方法移至相关的收集类别:

<?php

declare(strict_types=1);

class Ratings {
  private $ratings;

  public function __construct(float ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverage() : float {
    if (empty($this->ratings)) {
      return 0;
    }

    $total = 0;

    foreach ($this->ratings as $rating) {
      $total += $rating;
    }

    return $total / count($this->ratings);
  }
}

Notice how we’re still using a list of typed arguments with a variable length in our constructor, which saves us the trouble of looping over each rating to check its type.

请注意,我们仍然在构造函数中使用长度可变的类型化参数列表,这为我们省去了遍历每个等级以检查其类型的麻烦。

If we wanted the ability to use this collection class in foreach loops, we’d simply have to implement the IteratorAggregate interface:

如果我们希望能够在foreach循环中使用此集合类,则只需实现IteratorAggregate接口即可:

<?php

declare(strict_types=1);

class Ratings implements IteratorAggregate {
  private $ratings;

  public function __construct(float ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverage() : float { /* ... */ }

  public function getIterator() {
     return new ArrayIterator($this->ratings);
  }
}

Moving on, we can also create a collection for our list of air dates:

继续,我们还可以为我们的播出日期列表创建一个集合:

<?php

class AirDates implements IteratorAggregate {
  private $dates;

  public function __construct(\DateTimeImmutable ...$dates) {
    $this->dates = $dates;
  }

  public function getIterator() {
     return new ArrayIterator($this->airdates);
  }
}

Putting all the pieces of the puzzle together in the Movie class, we can now inject two separately typed collections in our constructor. Additionally we can define more specific return types than “array” on our get methods:

将难题的所有部分放到Movie类中,现在我们可以在构造函数中注入两个单独键入的集合。 另外,我们可以在get方法上定义比“数组”更具体的返回类型:

<?php

class Movie {
  private $dates;
  private $ratings;

  public function __construct(AirDates $airdates, Ratings $ratings) {
    $this->airdates = $airdates;
    $this->ratings = $ratings;
  }

  public function getAirDates() : AirDates {
    return $this->airdates;
  }

  public function getRatings() : Ratings {
    return $this->ratings;
  }
}

使用值对象进行自定义验证 (Using Value Objects for Custom Validation)

If we wanted to add extra validation to our ratings we could still go one step further, and define a Rating value object with some custom constraints. For example, a rating could be limited between 0 and 5:

如果我们想对评级添加额外的验证,我们仍然可以再走一步,并定义带有一些自定义约束的评级值对象。 例如,等级可以限制在0到5之间:

<?php

declare(strict_types=1);

class Rating {
  private $value;

  public function __construct(float $value) {
    if ($value < 0 || $value > 5) {
      throw new \InvalidArgumentException('A rating should always be a number between 0 and 5!');
    }

    $this->value = $value;
  }

  public function getValue() : float {
    return $this->value;
  }
}

Back in our Ratings collection class, we would only have to do some minor alterations to use these value objects instead of floats:

回到我们的Ratings集合类中,我们只需要做一些小改动就可以使用这些值对象而不是浮点数:

<?php

class Ratings implements IteratorAggregate {
  private $ratings;

  public function __construct(Rating ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverage() : Rating {
    if (empty($this->ratings)) {
      return new Rating(0);
    }

    $total = 0;

    foreach ($this->ratings as $rating) {
      $total += $rating->getValue();
    }

    $average = $total / count($this->ratings);
    return new Rating($average);
  }

  public function getIterator() { /* ... */ }
}

This way we get additional validation of individual collection members, still without having to loop over each injected object.

这样,我们仍然可以对单个集合成员进行额外的验证, 而不必循环遍历每个注入的对象

优点 (Advantages)

Typing out these separate collection classes and value object may seem like a lot of work, but they have several advantages over generic arrays and scalar values:

键入这些单独的集合类和值对象可能看起来很繁琐,但是与通用数组和标量值相比,它们具有多个优点:

  • Easy type validation in one place. We never have to manually loop over an array to validate the types of our collection members;

    一处轻松进行类型验证。 我们不必手动遍历数组来验证集合成员的类型。

  • Wherever we use these collections and value objects in our application, we know that their values have always been validated upon construction. For example, any Rating will always be between 0 and 5;

    无论我们在应用程序中在何处使用这些集合和值对象,我们都知道它们的值始终在构造时经过验证。 例如,任何评级将始终在0到5之间;

  • We can easily add custom logic per collection and/or value object. For example the getAverage() method, which we can re-use throughout our whole application;

    我们可以轻松地为每个集合和/或值对象添加自定义逻辑。 例如, getAverage()方法,我们可以在整个应用程序中重复使用该方法;

  • We get the possibility to inject multiple typed lists in a single function or method, which we cannot do using the ... token without injecting the values in collection classes first;

    我们有可能在单个函数或方法中注入多个类型列表,而如果不首先注入集合类中的值,则无法使用...标记来实现。

  • There are significantly reduced odds of mixing up arguments in method signatures. For example, when we want to inject both a list of ratings and a list of air dates, the two could easily get mixed up by accident upon construction when using generic arrays;

    方法签名中混合参数的几率大大降低。 例如,当我们要同时输入一个评级列表和一个播出日期列表时,在使用通用数组时,很容易在构建时偶然将两者混淆。

那编辑呢? (What about edits?)

By now you might be wondering how you could make changes to the values of your collections and value objects after initial construction.

到目前为止,您可能想知道在初始构造后如何更改集合和值对象的值。

While we could add methods to facilitate edits, this would quickly become cumbersome because we would have to duplicate most methods on each collection to keep the advantage of type hints. For example, an add() method on Ratings should only accept a Rating object, while an add() method on AirDates should only accept a DateTimeImmutable object. This makes interfacing and/or re-use of these methods very hard.

尽管我们可以添加方法来简化编辑,但是由于要保留类型提示的优势,我们必须在每个集合上复制大多数方法,因此很快就会变得麻烦。 例如, Ratingsadd()方法仅应接受Rating对象,而AirDatesadd()方法应仅接受DateTimeImmutable对象。 这使得这些方法的接口和/或重用非常困难。

Instead, we could simply keep our collections and value objects immutable, and convert them to their primitive types when we need to make changes. After we’re done making changes, we can simple re-construct any necessary collections or value objects with the updated values. Upon (re-)construction all types would be validated again, along with any extra validation we might have defined.

相反,我们可以简单地使集合和值对象保持不变,并在需要进行更改时将它们转换为原始类型。 进行更改后,我们可以使用更新后的值简单地重新构造任何必要的集合或值对象。 在(重新)构造后,所有类型都将再次进行验证,以及我们可能已定义的任何其他验证。

For example, we could add a simple toArray() method to our collections, and make changes like this:

例如,我们可以向集合中添加一个简单的toArray()方法,并进行如下更改:

<?php

// Initial Ratings collection
$ratings = new Ratings(
  new Rating(1.5),
  new Rating(3.5),
  new Rating(2.5)
);

// Convert the collection to an array.
$ratingsArray = $ratings->toArray();

// Remove the 2nd rating.
unset($ratingsArray[1]);
$ratingsArray = array_values($ratingsArray);

// Back to a (new) Ratings collection
$updatedRatings = new Ratings(...$ratingsArray);

This way we can also re-use existing array functionality like array_filter().

这样,我们还可以重用现有的数组功能,例如array_filter()

If we really needed to do edits on the collection objects themselves, we could add the necessary methods on a need-to-have basis wherever they are required. But keep in mind that most of those will also have to do type validation of the given argument(s), so it’s hard to re-use them across all different collection classes.

如果确实需要对集合对象本身进行编辑,则可以根据需要在需要的地方添加必要的方法。 但是请记住,其中大多数还必须对给定参数进行类型验证,因此很难在所有不同的集合类中重用它们。

重用通用方法 (Re-Using Generic Methods)

As you may have noticed we are still getting some code duplication across our collection classes by implementing both toArray() and getIterator() on all of them. Luckily these methods are generic enough to move to a generic parent class, as they both simply return the injected array:

您可能已经注意到,通过在所有集合类上同时实现toArray()getIterator() ,我们仍然可以在集合类上实现一些代码重复。 幸运的是,这些方法足够通用,可以移动到通用父类 ,因为它们都只是返回注入的数组:

<?php

abstract class GenericCollection implements IteratorAggregate
{
  protected $values;

  public function toArray() : array {
    return $this->values;
  }

  public function getIterator() {
    return new ArrayIterator($this->values);
  }
}

All we would be left with in our collection class would be type validation in the constructor, and any optional extra logic that is specific to that collection, like this:

我们在集合类中所剩下的就是构造函数中的类型验证,以及该集合所特有的任何可选的额外逻辑,例如:

<?php

class Ratings extends GenericCollection
{
  public function __construct(Rating ...$ratings) {
    $this->values = $ratings;
  }

  public function getAverage() : Rating { /* ... */ }
}

Optionally we could make our collection final, to prevent any child classes from messing with the values property in ways that could undo our type validation.

(可选)我们可以将集合设为final ,以防止任何子类以可能撤消类型验证的方式与values属性混淆。

结论 (Conclusion)

While still far from perfect, it has steadily been getting easier to work with type validation in collections and value objects with recent releases of PHP.

尽管还远远不够完美,但是在最近PHP版本中,使用集合中的类型验证和值对象的方法一直在变得越来越容易。

Ideally we’d get some form of generics in a future version of PHP to further facilitate the creation of re-usable collection classes.

理想情况下,我们将在将来PHP版本中获得某种形式的泛型 ,以进一步促进可重用集合类的创建。

A feature that would greatly improve the usage of value objects would be the ability to cast an object to different primitive types, in addition to string. This could easily be implemented by adding extra magic methods comparable to __toString(), like __toInt(), __toFloat(), etc.

可以大大改善值对象使用率的功能是将对象转换为除字符串之外的其他原始类型的功能。 这可以通过添加与__toString()类似的额外魔术方法轻松实现,例如__toInt()__toFloat()等。

Luckily there are some RFCs in progress to possibly implement both features in later versions, so fingers crossed! 🤞

幸运的是,有一些RFC正在实施中,可能在以后的版本中同时实现这两个功能,所以请多指教! 🤞



If you found this tutorial helpful, please visit the original post on Medium and give it some ❤️. If you have any feedback, questions, or comments, please leave them below or as a response on the original post.

如果您认为本教程有帮助,请访问Medium上的原始文章并给它一些❤️。 如果您有任何反馈,问题或意见,请将其留在下面或作为原始帖子的回复。

翻译自: https://www.sitepoint.com/creating-strictly-typed-arrays-collections-php/

php数组和集合

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值