Say No to Loop!

Paste_Image.png

本文会介绍下Eloquent\Collection,这个是什么呢?这是我们平常使用Eloquent中get,all返回的结果集。

collection缘来

我们首先来看一段简单的代码:

$books = Book::all();
$titles = [];
foreach ($books as $book){
  if ($book->pages_count > 8){
    $titles[] =  $book->title;
  }
}

这段代码意图其实非常明确,就是获取超过8的书名,再看下面一段代码:

$titles = [];
foreach ($books as $book){
  if ($book->publisher_id == 2){
    $titles[] =  $book->title;
  }
}

此处是获取作者是2的书名,所有这些代码都有同样的loop逻辑,我们完全可以抽取出来,于是就有了下面的函数:

function map($input, $func)
{
    $result = [];
    foreach ($input as $each)
    {
        $result[] = $func($each);
    }
    return $result;
}
map($books, function($book){
  if ($book->publisher_id == 2){
    return $book->title;
  }
});

这只是展示了一个简单的map模式,还有其他更多方便的集合操作方法,我们将其抽取出来,于是就出现了Collection,网上有个讲Collection的课程,不过太贵了,买不起。

其实Collection的总体思想感觉就是函数式编程,Tell, Don’t Ask,客户端在使用上不再是想着怎么做how,而是想着what to do,一直有个神一样存在的系列文章没去读,今天看到collection的文章,有了冲动去看的,文章地址:Category Theory for Programmers: The Preface,等最近看完orm系列就去看这个的。我们还是接着讲collection。

collection使用

在使用collection的原则上,我们遵守当代码出现loop的时候,我们就停下来想下,是否可以通过collection来解决。

first

三种使用方式

    public function testFirstReturnsFirstItemInCollection()
    {
        $c = new Collection(['foo', 'bar']);
        $this->assertEquals('foo', $c->first());
    }

    public function testFirstWithCallback()
    {
        $data = new Collection(['foo', 'bar', 'baz']);
        $result = $data->first(function ($value) {
            return $value === 'bar';
        });
        $this->assertEquals('bar', $result);
    }

    public function testFirstWithCallbackAndDefault()
    {
        $data = new Collection(['foo', 'bar']);
        $result = $data->first(function ($value) {
            return $value === 'baz';
        }, 'default');
        $this->assertEquals('default', $result);
    }

last

和first使用方式相同

    public function testLastReturnsLastItemInCollection()
    {
        $c = new Collection(['foo', 'bar']);
        $this->assertEquals('bar', $c->last());
    }

    public function testLastWithCallback()
    {
        $data = new Collection([100, 200, 300]);
        $result = $data->last(function ($value) {
            return $value < 250;
        });
        $this->assertEquals(200, $result);
        $result = $data->last(function ($value, $key) {
            return $key < 2;
        });
        $this->assertEquals(200, $result);
    }

    public function testLastWithCallbackAndDefault()
    {
        $data = new Collection(['foo', 'bar']);
        $result = $data->last(function ($value) {
            return $value === 'baz';
        }, 'default');
        $this->assertEquals('default', $result);
    }

map

map是对loop的抽离,对于集合中每个元素做完操作后,再返回新的元素。

    public function testMap()
    {
        $data = new Collection(['first' => 'taylor', 'last' => 'otwell']);
        $data = $data->map(function ($item, $key) {
            return $key.'-'.strrev($item);
        });
        $this->assertEquals(['first' => 'first-rolyat', 'last' => 'last-llewto'], $data->all());
    }

each

遍历元组进行操作,不返回元素操作后的结果,当遇到返回false的时候,结束遍历。

    public function testEach()
    {
        $c = new Collection($original = [1, 2, 'foo' => 'bar', 'bam' => 'baz']);

        $result = [];
        $c->each(function ($item, $key) use (&$result) {
            $result[$key] = $item;
        });
        $this->assertEquals($original, $result);

        $result = [];
        $c->each(function ($item, $key) use (&$result) {
            $result[$key] = $item;
            if (is_string($key)) {
                return false;
            }
        });
        $this->assertEquals([1, 2, 'foo' => 'bar'], $result);
    }

filter

遍历集合,只将符合条件的留下,集合中元素的性质不会变,如果集合中是product,返回的也是product,不会像map那样,返回price

    public function testFilter()
    {
        $c = new Collection([['id' => 1, 'name' => 'Hello'], ['id' => 2, 'name' => 'World']]);
        $this->assertEquals([1 => ['id' => 2, 'name' => 'World']], $c->filter(function ($item) {
            return $item['id'] == 2;
        })->all());

        $c = new Collection(['', 'Hello', '', 'World']);
        $this->assertEquals(['Hello', 'World'], $c->filter()->values()->toArray());

        $c = new Collection(['id' => 1, 'first' => 'Hello', 'second' => 'World']);
        $this->assertEquals(['first' => 'Hello', 'second' => 'World'], $c->filter(function ($item, $key) {
            return $key != 'id';
        })->all());
    }

reduce

reduce将一个集合中的元素做遍历,返回为一个单子的元素

public function testReduce()
{
    $data = new Collection([1, 2, 3]);
    $this->assertEquals(6, $data->reduce(function ($carry, $element) {
        return $carry += $element;
    }));
}

flatten

flatten意为平坦,可以将任意嵌套的array变为同层级的,通过参数depth,可以指定平坦的层级

public function testFlatten()
{
    // Flat arrays are unaffected
    $c = new Collection(['#foo', '#bar', '#baz']);
    $this->assertEquals(['#foo', '#bar', '#baz'], $c->flatten()->all());

    // Nested arrays are flattened with existing flat items
    $c = new Collection([['#foo', '#bar'], '#baz']);
    $this->assertEquals(['#foo', '#bar', '#baz'], $c->flatten()->all());

    // Sets of nested arrays are flattened
    $c = new Collection([['#foo', '#bar'], ['#baz']]);
    $this->assertEquals(['#foo', '#bar', '#baz'], $c->flatten()->all());

    // Deeply nested arrays are flattened
    $c = new Collection([['#foo', ['#bar']], ['#baz']]);
    $this->assertEquals(['#foo', '#bar', '#baz'], $c->flatten()->all());
}
public function testFlattenWithDepth()
{
  // No depth flattens recursively
  $c = new Collection([['#foo', ['#bar', ['#baz']]], '#zap']);
  $this->assertEquals(['#foo', '#bar', '#baz', '#zap'], $c->flatten()->all());

  // Specifying a depth only flattens to that depth
  $c = new Collection([['#foo', ['#bar', ['#baz']]], '#zap']);
  $this->assertEquals(['#foo', ['#bar', ['#baz']], '#zap'], $c->flatten(1)->all());

  $c = new Collection([['#foo', ['#bar', ['#baz']]], '#zap']);
  $this->assertEquals(['#foo', '#bar', ['#baz'], '#zap'], $c->flatten(2)->all());
}

FlatMap

flatMap类似于做了先map后flat的操作

public function testFlatMap()
{
    $data = new Collection([
        ['name' => 'taylor', 'hobbies' => ['programming', 'basketball']],
        ['name' => 'adam', 'hobbies' => ['music', 'powerlifting']],
    ]);
    $data = $data->flatMap(function ($person) {
        return $person['hobbies'];
    });
    $this->assertEquals(['programming', 'basketball', 'music', 'powerlifting'], $data->all());
}

zip

吧两个结构一样的array像拉拉链一样做合并

public function testZip()
{
    $c = new Collection([1, 2, 3]);
    $c = $c->zip(new Collection([4, 5, 6]));
    $this->assertInstanceOf(Collection::class, $c);
    $this->assertInstanceOf(Collection::class, $c[0]);
    $this->assertInstanceOf(Collection::class, $c[1]);
    $this->assertInstanceOf(Collection::class, $c[2]);
    $this->assertCount(3, $c);
    $this->assertEquals([1, 4], $c[0]->all());
    $this->assertEquals([2, 5], $c[1]->all());
    $this->assertEquals([3, 6], $c[2]->all());

    $c = new Collection([1, 2, 3]);
    $c = $c->zip([4, 5, 6], [7, 8, 9]);
    $this->assertCount(3, $c);
    $this->assertEquals([1, 4, 7], $c[0]->all());
    $this->assertEquals([2, 5, 8], $c[1]->all());
    $this->assertEquals([3, 6, 9], $c[2]->all());

    $c = new Collection([1, 2, 3]);
    $c = $c->zip([4, 5, 6], [7]);
    $this->assertCount(3, $c);
    $this->assertEquals([1, 4, 7], $c[0]->all());
    $this->assertEquals([2, 5, null], $c[1]->all());
    $this->assertEquals([3, 6, null], $c[2]->all());
}

pluck

pluck接受两个参数,如果传递了第二个参数,则以第二个参数为key

public function testPluckWithArrayAndObjectValues()
{
    $data = new Collection([(object) ['name' => 'taylor', 'email' => 'foo'], ['name' => 'dayle', 'email' => 'bar']]);
    $this->assertEquals(['taylor' => 'foo', 'dayle' => 'bar'], $data->pluck('email', 'name')->all());
    $this->assertEquals(['foo', 'bar'], $data->pluck('email')->all());
}

更详细的可以参考https://laravel.tw/docs/5.2/collections

一些建议

我们使用collection的取值的时候,如果没有对应的值,我们可以提供default值,此时可以在default中直接抛出异常

return $this->checkers->first(function ($i, $checker) use ( $file) {
    return $checker->canCheck($file);
}, function () {
    throw new Exception("No matching style checker found!");
});

我们有时候为了连贯操作,即使前一个出错了,我们也不希望返回一个null object,我们希望能返回一个空对象,但是这个对象实现了一个空操作,意图如下:

$this->getObject($input)->check();

此处getObject($input)可能返回是一个实现了check操作的空对象,这时候就可以使用Macroable trait 的东西。

public function testMacroable()
{
    // Foo() macro : unique values starting with A
    Collection::macro('foo', function () {
        return $this->filter(function ($item) {
            return strpos($item, 'a') === 0;
        })
            ->unique()
            ->values();
    });

    $c = new Collection(['a', 'a', 'aa', 'aaa', 'bar']);

    $this->assertSame(['a', 'aa', 'aaa'], $c->foo()->all());
}

更多内容大家可以去看文章Refactoring to Collection — Notes

参考

Refactoring to Collection — Notes

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容