Say No to Loop!

作者: 小聪明李良才 | 来源:发表于2016-12-02 17:48 被阅读255次
    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

    相关文章

      网友评论

      本文标题:Say No to Loop!

      本文链接:https://www.haomeiwen.com/subject/asjqmttx.html