YaLinqo port de .NET 4 LINQ vers PHP – Par l’exemple

Here, you will find a few ports of Microsoft Developer Network’s 101 LINQ Samples using YaLinqo library, a LINQ port for PHP.

Restriction Operators [where],
Projection Operators [select],
Partitioning Operators [take, skip, takewhile, skipwhile],
Ordering Operators [orderby, orderbydescending, thenby],
Grouping Operators [groupby],
Set Operators [distinct, union, intersect, except],
Conversion Operators [toarray, tolist, todictionnary, oftype],
Element Operators [first,firstordefault,elementat],
Quantifiers [any, all],
Aggregate Operators [count, sum, min, max, average, aggregate],
Join Operators [join, groupjoin]

YaLinqo par l’exemple – Introduction

LINQ (Language Integrated Query) est un langage de requête qui étend la syntaxe des langages C# et Visual Basic, c’est un outil à la fois puissant et élégant que l’on aimerai utiliser en dehors de ces langages.

Plusieurs tentatives ont été faites pour porter ces possibilités sur le langage PHP, parmi lesquelles une assez réussie, qui apporte beaucoup d’agréments au développeur car elle est assez intuitive si l’on a utilisé LINQ. Elle est bien documentée pour l’auto-complétion des méthodes dans l’IDE, et n’impacte pas trop les performances par rapport au confort d’utilisation et aux gains sur la souplesse et la maintenabilité des codes sources.

Ce document est un guide pratique, un tutoriel à l’utilisation de la librairie YaLinqo à laquelle il manque des exemples pratiques. Dans un premier temps j’ai repris certains des exemples d’utilisation de LINQ par Microsoft, ceux ci peuvent servir de référence. Certains exemples sont redondants, et je ne les ai pas repris, d’autres exemples démontrent plus cruellement les limitations de la librairie et je n’ai pas pu les transcrire, soit parce-que ce n’est pas possible, soit parce-que je n’ai pas compris et pris le temps de comprendre comment utiliser la librairie pour ces cas d’utilisation.

Au delà de ces exemples simples nous verrons des cas pratiques sur une structure de données plus complexe. Je vous montrerai comment utiliser la librairie pour exploiter les métadonnées de vos collections de photographies et collections musicales extraites à partir de la librairie GetID3.

Faites attention à respecter l’emploi du caractère simple ‘ et non pas du double « , depuis les exemples C# pensez à doubler ==>, préfixer vos variables par un $, ou encore utiliser -> pour vos appels de méthodes.

Installation

composer require athari/yalinqo ~2.0

Pour exécuter certains de ces exemples vous aurez besoin de quelques données produit très simples !

Définitions

class Product
{

    private $id;
    private $unitsInStock = 0;
    private $productName = '';
    private $unitPrice = 0;
    private $category;

    function getId()
    {
        return $this->id;
    }

    function getUnitsInStock()
    {
        return $this->unitsInStock;
    }

    function getProductName()
    {
        return $this->productName;
    }

    function getUnitPrice()
    {
        return $this->unitPrice;
    }

    function getCategory()
    {
        return $this->category;
    }

    function __construct($id, $productName, $unitsInStock, $unitPrice, $category)
    {
        $this->id = $id;
        $this->productName = $productName;
        $this->unitsInStock = (int) $unitsInStock;
        $this->unitPrice = (int) $unitPrice;
        $this->category = $category;
    }

}

$products = [
    new Product(1, "Chef Anton's Gumbo Mix", 0, 2, 'meat'),
    new Product(2, "Alice Mutton", 0, 5, 'meat'),
    new Product(3, "Rostbratwurst", 5, 3, 'meat'),
    new Product(4, "Gorgonzola Telino", 6, 6, 'cheese'),
    new Product(5, "Ikura", 0, 3, 'fish'),
    new Product(6, "Teatime Chocolate Biscuits", 2, 3, 'dessert'),
    new Product(7, "Chartreuse verte", 0, 2, 'fuel'),
    new Product(8, "Camembert Pierrot", 2, 7, 'cheese'),
];

Restriction Operators

Description

Where – Simple 1
This sample uses where to find all elements of an array less than 5

$numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

$lowNums = from($numbers)
        ->where('$n ==> $n < 5') ->toList();

Resultat

Array
(
    [0] => 4
    [1] => 1
    [2] => 3
    [3] => 2
    [4] => 0
)

Where – Simple 2
This sample uses where to find all products that are out of stock

$soldOutProducts = from($products)
        ->where('$p ==> $p->getUnitsInStock() == 0')
        ->toList();

Resultat

Array
(
    [0] => Product Object
        (
            [id:Product:private] => 1
            [unitsInStock:Product:private] => 0
            [productName:Product:private] => Chef Anton's Gumbo Mix
            [unitPrice:Product:private] => 2
            [category:Product:private] => meat
        )

    [1] => Product Object
        (
            [id:Product:private] => 2
            [unitsInStock:Product:private] => 0
            [productName:Product:private] => Alice Mutton
            [unitPrice:Product:private] => 5
            [category:Product:private] => meat
        )

    [2] => Product Object
        (
            [id:Product:private] => 5
            [unitsInStock:Product:private] => 0
            [productName:Product:private] => Ikura
            [unitPrice:Product:private] => 3
            [category:Product:private] => fish
        )

    [3] => Product Object
        (
            [id:Product:private] => 7
            [unitsInStock:Product:private] => 0
            [productName:Product:private] => Chartreuse verte
            [unitPrice:Product:private] => 2
            [category:Product:private] => fuel
        )

)

Where – Simple 3
This sample uses where to find all products that are in stock and cost more than 3.00 per unit

$expensiveInStockProducts = from($products)
        ->where('$p ==> ($p->getUnitsInStock() > 0 && $p->getUnitPrice() > 3) ')
        ->toList();

Resultat

Array
(
    [0] => Product Object
        (
            [id:Product:private] => 4
            [unitsInStock:Product:private] => 6
            [productName:Product:private] => Gorgonzola Telino
            [unitPrice:Product:private] => 6
            [category:Product:private] => cheese
        )

    [1] => Product Object
        (
            [id:Product:private] => 8
            [unitsInStock:Product:private] => 2
            [productName:Product:private] => Camembert Pierrot
            [unitPrice:Product:private] => 7
            [category:Product:private] => cheese
        )

)

Where – Indexed
This sample demonstrates an indexed Where clause that returns digits whose name is shorter than their value

$digits = [ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];

$shortDigits = from($digits)
        ->where('$v, $k ==> strlen($v) < $k') ->toArray();

Resultat

Array
(
    [5] => five
    [6] => six
    [7] => seven
    [8] => eight
    [9] => nine
)

Projection Operators

Description
Select – Simple 1
This sample uses select to produce a sequence of ints one higher than those in an existing array of ints

$numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

$numsPlusOne = from($numbers)
        ->select('$n ==> $n + 1')
        ->toList();

Resultat

Array
(
    [0] => 6
    [1] => 5
    [2] => 2
    [3] => 4
    [4] => 10
    [5] => 9
    [6] => 7
    [7] => 8
    [8] => 3
    [9] => 1
)

Select – Simple 2
This sample uses select to return a sequence of just the names of a list of products

$productNames = from($products)
        ->select('$p ==> $p->getProductName()')
        ->toList();

Resultat

Array
(
    [0] => Chef Anton's Gumbo Mix
    [1] => Alice Mutton
    [2] => Rostbratwurst
    [3] => Gorgonzola Telino
    [4] => Ikura
    [5] => Teatime Chocolate Biscuits
    [6] => Chartreuse verte
    [7] => Camembert Pierrot
)

Select – Anonymous Types 1
This sample uses select to produce a sequence of the uppercase and lowercase versions of each word in the original array

$words = [ "aPPLE", "BlUeBeRrY", "cHeRry"];

$upperLowerWords = from($words)
        ->select('$w ==> (object)array("upper" => strtoupper($w), "lower" => strtolower($w))')
        ->toList();

Resultat

Array
(
    [0] => stdClass Object
        (
            [upper] => APPLE
            [lower] => apple
        )

    [1] => stdClass Object
        (
            [upper] => BLUEBERRY
            [lower] => blueberry
        )

    [2] => stdClass Object
        (
            [upper] => CHERRY
            [lower] => cherry
        )

)

Partitioning Operators

Description
Take – Simple
This sample uses Take to get only the first 3 elements of the array

$numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

$first3Numbers = from($numbers)
        ->take(3)
        ->toList();

Resultat

Array
(
    [0] => 5
    [1] => 4
    [2] => 1
)

Skip – Simple
This sample uses Skip to get all but the first 4 elements of the array

$allButFirst4Numbers = from($numbers)
        ->skip(4)
        ->toList();

Resultat

Array
(
    [0] => 9
    [1] => 8
    [2] => 6
    [3] => 7
    [4] => 2
    [5] => 0
)

TakeWhile – Simple
This sample uses TakeWhile to return elements starting from the beginning of the array until a number is hit that is not less than 6

$firstNumbersLessThan6 = from($numbers)
        ->takeWhile('$n ==> $n < 6') ->toList();

Resultat

Array
(
    [0] => 5
    [1] => 4
    [2] => 1
    [3] => 3
)

TakeWhile – Indexed
This sample uses TakeWhile to return elements starting from the beginning of the array until a number is hit that is less than its position in the array

$firstSmallNumbers = from($numbers)
        ->takeWhile('$n, $index ==> $n >= $index')
        ->toList();

Resultat

Array
(
    [0] => 5
    [1] => 4
)

SkipWhile – Simple
This sample uses SkipWhile to get the elements of the array starting from the first element divisible by 3

$allButFirst3Numbers = from($numbers)
        ->skipWhile('$n ==> $n % 3 != 0')
        ->toList();

Resultat

Array
(
    [0] => 3
    [1] => 9
    [2] => 8
    [3] => 6
    [4] => 7
    [5] => 2
    [6] => 0
)

Ordering Operators

Description
OrderBy – Simple 1
This sample uses orderby to sort a list of words alphabetically

$words = [ "cherry", "apple", "blueberry"];
$sortedWords = from($words)
        ->orderBy('$v')
        ->toList();

Resultat

Array
(
    [0] => apple
    [1] => blueberry
    [2] => cherry
)

OrderBy – Simple 2
This sample uses orderby to sort a list of words by length

$sortedWords = from($words)
        ->orderBy('$v ==> strlen($v)')
        ->select('$v ==> [$v => strlen($v)]')
        ->toList();

Resultat

Array
(
    [0] => Array
        (
            [apple] => 5
        )

    [1] => Array
        (
            [cherry] => 6
        )

    [2] => Array
        (
            [blueberry] => 9
        )

)

OrderBy – Simple 3
This sample uses orderby to sort a list of products by name

$sortedProducts = from($products)
        ->orderBy('$p ==> $p->getProductName()')
        ->select('$p ==> $p->getProductName()')
        ->toList();

Resultat

Array
(
    [0] => Alice Mutton
    [1] => Camembert Pierrot
    [2] => Chartreuse verte
    [3] => Chef Anton's Gumbo Mix
    [4] => Gorgonzola Telino
    [5] => Ikura
    [6] => Rostbratwurst
    [7] => Teatime Chocolate Biscuits
)

OrderBy – Comparer
This sample uses an OrderBy clause with a custom comparer to do a case-insensitive sort of the words in an array

$sortedProducts = from($products)
        ->orderBy('$p ==> $p', function ($a, $b) {
            return $a->getUnitPrice() > $b->getUnitPrice();
        })
        ->select('$p ==> [$p->getProductName() => $p->getUnitPrice()]')
        ->toList();

Resultat

Array
(
    [0] => Array
        (
            [Chef Anton's Gumbo Mix] => 2
        )

    [1] => Array
        (
            [Chartreuse verte] => 2
        )

    [2] => Array
        (
            [Teatime Chocolate Biscuits] => 3
        )

    [3] => Array
        (
            [Ikura] => 3
        )

    [4] => Array
        (
            [Rostbratwurst] => 3
        )

    [5] => Array
        (
            [Alice Mutton] => 5
        )

    [6] => Array
        (
            [Gorgonzola Telino] => 6
        )

    [7] => Array
        (
            [Camembert Pierrot] => 7
        )

)

OrderByDescending – Simple 1
This sample uses orderby and descending to sort a list of doubles from highest to lowest

$doubles = [ 1.7, 2.3, 1.9, 4.1, 2.9];

$sortedDoubles = from($doubles)
        ->orderByDescending()
        ->toList();

Resultat

Array
(
    [0] => 4.1
    [1] => 2.9
    [2] => 2.3
    [3] => 1.9
    [4] => 1.7
)

ThenBy – Simple
This sample uses a compound orderby to sort a list of digits, first by length of their name, and then alphabetically by the name itself

$words = [ "aPPLE", "AbAcUs", "bRaNcH", "BlUeBeRrY", "ClOvEr", "cHeRry"];

$sortedWords = from($words)
        ->orderBy('$w ==> strlen($w)')
        ->thenBy('$w ==> strtoupper($w)')
        ->toList();

Resultat

Array
(
    [0] => aPPLE
    [1] => AbAcUs
    [2] => bRaNcH
    [3] => cHeRry
    [4] => ClOvEr
    [5] => BlUeBeRrY
)

Grouping Operators

Description
GroupBy – Simple 1
This sample uses group by to partition a list of numbers by their remainder when divided by 5

$numberGroups = from($numbers)
        ->groupBy('$n ==> $n % 5') 
        ->toList();

Resultat

Array
(
    [0] => Array
        (
            [0] => 5
            [1] => 0
        )

    [1] => Array
        (
            [0] => 4
            [1] => 9
        )

    [2] => Array
        (
            [0] => 1
            [1] => 6
        )

    [3] => Array
        (
            [0] => 3
            [1] => 8
        )

    [4] => Array
        (
            [0] => 7
            [1] => 2
        )

)

GroupBy – Simple 2
This sample uses group by to partition a list of words by their first letter

$words = [ "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese"];

$wordGroups = from($words)
        ->groupBy('$w ==> $w[0]')
        ->select('$v, $k ==> (object)["firstLetter" => $k, "words" => $v]')
        ->toArray();

Resultat

Array
(
    [b] => stdClass Object
        (
            [firstLetter] => b
            [words] => Array
                (
                    [0] => blueberry
                    [1] => banana
                )

        )

     => stdClass Object
        (
            [firstLetter] => c
            [words] => Array
                (
                    [0] => chimpanzee
                    [1] => cheese
                )

        )

    [a] => stdClass Object
        (
            [firstLetter] => a
            [words] => Array
                (
                    [0] => abacus
                    [1] => apple
                )

        )

)

GroupBy – Simple 3
This sample uses group by to partition a list of products by category

$orderGroups = from($products)
        ->groupBy('$p ==> $p->getCategory()')
        ->select('$v, $k ==> (object)["category" => $k, "products" => $v]')
        ->toArray();

Resultat

Array
(
    [meat] => stdClass Object
        (
            [category] => meat
            [products] => Array
                (
                    [0] => Product Object
                        (
                            ...
                        )

                    [1] => Product Object
                        (
                            ...
                        )

                    [2] => Product Object
                        (
                            ...
                        )

                )

        )

    [cheese] => stdClass Object
        (
            [category] => cheese
            [products] => Array
                (
                    [0] => Product Object
                        (
                            ...
                        )

                    [1] => Product Object
                        (
                            ...
                        )

                )

        )

    [fish] => stdClass Object
        (
            [category] => fish
            [products] => Array
                (
                    [0] => Product Object
                        (
                            ...
                        )

                )

        )

    [dessert] => stdClass Object
        (
            [category] => dessert
            [products] => Array
                (
                    [0] => Product Object
                        (
                            ...
                        )

                )

        )

    [fuel] => stdClass Object
        (
            [category] => fuel
            [products] => Array
                (
                    [0] => Product Object
                        (
                            ...
                        )

                )

        )

)

Set Operators

Description

Distinct – 1
This sample uses Distinct to remove duplicate elements in a sequence of factors of 300

$factorsOf300 = [ 2, 2, 3, 5, 5];

$uniqueFactors = from($factorsOf300)->distinct()->toList();

Resultat

Array
(
    [0] => 2
    [1] => 3
    [2] => 5
)

Distinct – 2
This sample uses Distinct to find the unique Category names

$categoryNames = from($products)
        ->select('$p ==> $p->getCategory()')
        ->distinct()
        ->toList();

Resultat

Array
(
    [0] => meat
    [1] => cheese
    [2] => fish
    [3] => dessert
    [4] => fuel
)

Union – 1
This sample uses Union to create one sequence that contains the unique values from both arrays

$numbersA = [ 0, 2, 4, 5, 6, 8, 9];
$numbersB = [ 1, 3, 5, 7, 8];

$uniqueNumbers = from($numbersA)
        ->union($numbersB)
        ->toList();

Resultat

Array
(
    [0] => 0
    [1] => 2
    [2] => 4
    [3] => 5
    [4] => 6
    [5] => 8
    [6] => 9
    [7] => 1
    [8] => 3
    [9] => 7
)

Intersect – 1
This sample uses Intersect to create one sequence that contains the common values shared by both arrays

$numbersA = [ 0, 2, 4, 5, 6, 8, 9];
$numbersB = [ 1, 3, 5, 7, 8];

$commonNumbers = from($numbersA)
        ->intersect($numbersB)
        ->toList();

Resultat

Array
(
    [0] => 5
    [1] => 8
)

Except – 1
This sample uses Except to create a sequence that contains the values from numbersAthat are not also in numbersB

$aOnlyNumbers = from($numbersA)
        ->except($numbersB)
        ->toList();

Resultat

Array
(
    [0] => 0
    [1] => 2
    [2] => 4
    [3] => 6
    [4] => 9
)

Conversion Operators

Description
ToArray vs ToList
This sample uses ToArray to immediately evaluate a sequence into an array and into a list (notice indices)

$doubles = [ 1.7, 2.3, 1.9, 4.1, 2.9];
$sortedDoubles = from($doubles)
        ->orderByDescending();

print_r($sortedDoubles->toArray());
print_r($sortedDoubles->toList());

Resultat

Array
(
    [3] => 4.1
    [4] => 2.9
    [1] => 2.3
    [2] => 1.9
    [0] => 1.7
)
Array
(
    [0] => 4.1
    [1] => 2.9
    [2] => 2.3
    [3] => 1.9
    [4] => 1.7
)

ToDictionary
This sample uses ToDictionary to immediately evaluate a sequence and a related key expression into a dictionary

$orderRecords = from($products)
        ->toDictionary('$p ==> $p->getProductName()', '$p ==> $p->getCategory()');

Resultat

Array
(
    [Chef Anton's Gumbo Mix] => meat
    [Alice Mutton] => meat
    [Rostbratwurst] => meat
    [Gorgonzola Telino] => cheese
    [Ikura] => fish
    [Teatime Chocolate Biscuits] => dessert
    [Chartreuse verte] => fuel
    [Camembert Pierrot] => cheese
)

OfType
This sample uses OfType to return only the elements of the array that are of a given type

$numbers = [ null, 1.0, "two", 3, "four", 5, "six", 7.0, $products[0], new \stdClass()];
# Eyes bleeding ? Actually I extended this sample to other types that doesn't look like numbers...

$ints = from($numbers)->ofType('int')->toList();
$floats = from($numbers)->ofType('float')->toList();
$products = from($numbers)
        ->ofType('object')
        ->where('$o ==> $o instanceof Product')
        ->toList();

print_r($ints);
print_r($floats);
print_r($products);

Resultat

Array
(
    [0] => 3
    [1] => 5
)
Array
(
    [0] => 1
    [1] => 7
)
ProductArray
(
    [0] => Product Object
        (
            [id:Product:private] => 1
            [unitsInStock:Product:private] => 0
            [productName:Product:private] => Chef Anton's Gumbo Mix
            [unitPrice:Product:private] => 2
            [category:Product:private] => meat
        )

)

Element Operators

Description
First – Simple
This sample uses First to return the first matching element as a Product, instead of as a sequence containing a Product

$product4 = from($products)
        ->where('$p ==> $p->getId() == 4')
        ->first();

Resultat

Product Object
(
    [id:Product:private] => 4
    [unitsInStock:Product:private] => 6
    [productName:Product:private] => Gorgonzola Telino
    [unitPrice:Product:private] => 6
    [category:Product:private] => cheese
)

First – Condition
This sample uses First to find the first element in the array that starts with ‘o’

$strings = [ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];

$startsWithO = from($strings)
        ->first('$s ==> $s[0] == "o"');

Resultat

one

FirstOrDefault – Simple
This sample uses FirstOrDefault to try to return the first element of the sequence, unless there are no elements, in which case the default value for that type is returne

$firstNumOrDefault = from($numbers)->firstOrDefault();

Resultat

0

FirstOrDefault – Condition
This sample uses FirstOrDefault to return the first product whose ProductID is 789 as a single Product object, unless there is no match, in which case null is returned

$product789 = from($products)
        ->firstOrDefault(null, '$p ==> $p->getId() == 789');

Resultat

null

ElementAt
This sample uses ElementAt to retrieve the second number greater than 5 from an array

$numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

$fourthLowNum = from($numbers)
        ->where('$n ==> $n > 5')
        ->toValues()
        ->elementAt(1);

Resultat

8

Quantifiers

Description
Any – Simple
This sample uses Any to determine if any of the words in the array contain the substring ‘ei’

$words = [ "believe", "relief", "receipt", "field"];

$iAfterE = from($words)
        ->any('$w ==> (false !== strrpos($w, "ei"))');

Resultat

true

All – Simple
This sample uses All to determine whether an array contains only odd numbers

$numbers = [ 1, 11, 3, 19, 41, 65, 19];

$onlyOdd = from($numbers)
        ->all('$n ==> $n % 2 == 1');

Resultat

true

All – Grouped
This sample uses All to return a grouped a list of products only for categories that have all of their products in stock
REM. Best way to do that with YaLinqo PHP ? code becomes hard to read

$productGroups = from($products)
        ->groupBy('$p ==> $p->getCategory()')
        ->toArray();

$productGroups = from($productGroups)
        ->where('$g,$k ==> from($g)->all(\'$p ==> $p->getUnitsInStock() > 0\')')
        ->toArray();

Resultat

Array
(
    [cheese] => Array
        (
            [0] => Product Object
                (
                    [id:Product:private] => 4
                    [unitsInStock:Product:private] => 6
                    [productName:Product:private] => Gorgonzola Telino
                    [unitPrice:Product:private] => 6
                    [category:Product:private] => cheese
                )

            [1] => Product Object
                (
                    [id:Product:private] => 8
                    [unitsInStock:Product:private] => 2
                    [productName:Product:private] => Camembert Pierrot
                    [unitPrice:Product:private] => 7
                    [category:Product:private] => cheese
                )

        )

    [dessert] => Array
        (
            [0] => Product Object
                (
                    [id:Product:private] => 6
                    [unitsInStock:Product:private] => 2
                    [productName:Product:private] => Teatime Chocolate Biscuits
                    [unitPrice:Product:private] => 3
                    [category:Product:private] => dessert
                )

        )

)

Aggregate Operators

Description
Count – Simple
This sample uses Count to get the number of unique factors of 300

$factorsOf300 = [ 2, 2, 3, 5, 5];

$uniqueFactors = from($factorsOf300)->distinct()->count();

Resultat

3

Count – Conditional
This sample uses Count to get the number of odd ints in the array

$numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

$oddNumbers = from($numbers)->count('$n ==> $n % 2 == 1');

Resultat

5

Count – Grouped
This sample uses Count to return a list of categories and how many products each has

$categoryCounts = from($products)
        ->groupBy('$p ==> $p->getCategory()')
        ->select('$v, $k ==> (object)["Category" => $k, "ProductCount" => from($v)->count()]')
        ->toList();

Resultat

Array
(
    [0] => stdClass Object
        (
            [Category] => meat
            [ProductCount] => 3
        )

    [1] => stdClass Object
        (
            [Category] => cheese
            [ProductCount] => 2
        )

    [2] => stdClass Object
        (
            [Category] => fish
            [ProductCount] => 1
        )

    [3] => stdClass Object
        (
            [Category] => dessert
            [ProductCount] => 1
        )

    [4] => stdClass Object
        (
            [Category] => fuel
            [ProductCount] => 1
        )

)

Sum – Simple
This sample uses Sum to get the total of the numbers in an array

$numSum = from($numbers)->sum();

Resultat

45

Sum – Projection
This sample uses Sum to get the total number of characters of all words in the array

$words = [ "cherry", "apple", "blueberry"];

$totalChars = from($words)->sum('$w ==> strlen($w)');

Resultat

20

Sum – Grouped
This sample uses Sum to get the total units in stock for each product category

$categories = from($products)
        ->groupBy('$p ==> $p->getCategory()')
        ->select('$v, $k ==> (object)["Category" => $k, "TotalUnitsInStock" => from($v)->sum(\'$p ==> $p->getUnitsInStock()\')]')
        ->toList();

Resultat

Array
(
    [0] => stdClass Object
        (
            [Category] => meat
            [TotalUnitsInStock] => 5
        )

    [1] => stdClass Object
        (
            [Category] => cheese
            [TotalUnitsInStock] => 8
        )

    [2] => stdClass Object
        (
            [Category] => fish
            [TotalUnitsInStock] => 0
        )

    [3] => stdClass Object
        (
            [Category] => dessert
            [TotalUnitsInStock] => 2
        )

    [4] => stdClass Object
        (
            [Category] => fuel
            [TotalUnitsInStock] => 0
        )

)

Min – Simple (min(), max() et average() sont interchangeables…)
This sample uses Min to get the lowest number in an array.

$minNum = from($numbers)->min();

Resultat

0

Min – Projection
This sample uses Min to get the length of the shortest word in an array

$shortestWord = from($words)->min('$w ==> strlen($w)');

Resultat

5

Min – Grouped
This sample uses Min to get the cheapest price among each category’s products

$categories = from($products)
        ->groupBy('$p ==> $p->getCategory()')
        ->select('$v, $k ==> (object)["Category" => $k, "CheapestPrice" => from($v)->min(\'$p ==> $p->getUnitPrice()\')]')
        ->toList();

Resultat

Array
(
    [0] => stdClass Object
        (
            [Category] => meat
            [CheapestPrice] => 2
        )

    [1] => stdClass Object
        (
            [Category] => cheese
            [CheapestPrice] => 6
        )

    [2] => stdClass Object
        (
            [Category] => fish
            [CheapestPrice] => 3
        )

    [3] => stdClass Object
        (
            [Category] => dessert
            [CheapestPrice] => 3
        )

    [4] => stdClass Object
        (
            [Category] => fuel
            [CheapestPrice] => 2
        )

)

Aggregate – Simple
This sample uses Aggregate to create a running product on the array that calculates the total product of all elements

$doubles = [ 1.7, 2.3, 1.9, 4.1, 2.9];

$product = from($doubles)
        ->aggregate('($runningProduct, $nextFactor) ==> $runningProduct * $nextFactor');

Resultat

88.33081

Aggregate – Seed
This sample uses Aggregate to create a running account balance that subtracts each withdrawal from the initial balance of 100, as long as the balance never drops below 0

$startBalance = 100.0;

$attemptedWithdrawals = [ 20, 10, 40, 50, 10, 70, 30];

$endBalance = from($attemptedWithdrawals)
        ->aggregate('($balance, $nextWithdrawal) ==> 
                (($nextWithdrawal <= $balance) ? ($balance - $nextWithdrawal) : $balance)', $startBalance);

Resultat

20

Join Operators

Description
Cross Join

$categories = [ "cherry", "apple", "blueberry", "cheese", "meat"];

$q = from($categories)
        ->join(
                from($products), '$c ==> $c', '$p ==> $p->getCategory()', '($c, $p) ==> array(
                    "category" => $c,
                    "product" => $p
                )'
        )
        ->toList();

Resultat

Array
(
    [0] => Array
        (
            [category] => cheese
            [product] => Product Object
                (
                    [id:Product:private] => 4
                    [unitsInStock:Product:private] => 6
                    [productName:Product:private] => Gorgonzola Telino
                    [unitPrice:Product:private] => 6
                    [category:Product:private] => cheese
                )

        )

    [1] => Array
        (
            [category] => cheese
            [product] => Product Object
                (
                    [id:Product:private] => 8
                    [unitsInStock:Product:private] => 2
                    [productName:Product:private] => Camembert Pierrot
                    [unitPrice:Product:private] => 7
                    [category:Product:private] => cheese
                )

        )

    [2] => Array
        (
            [category] => meat
            [product] => Product Object
                (
                    [id:Product:private] => 1
                    [unitsInStock:Product:private] => 0
                    [productName:Product:private] => Chef Anton's Gumbo Mix
                    [unitPrice:Product:private] => 2
                    [category:Product:private] => meat
                )

        )

    [3] => Array
        (
            [category] => meat
            [product] => Product Object
                (
                    [id:Product:private] => 2
                    [unitsInStock:Product:private] => 0
                    [productName:Product:private] => Alice Mutton
                    [unitPrice:Product:private] => 5
                    [category:Product:private] => meat
                )

        )

    [4] => Array
        (
            [category] => meat
            [product] => Product Object
                (
                    [id:Product:private] => 3
                    [unitsInStock:Product:private] => 5
                    [productName:Product:private] => Rostbratwurst
                    [unitPrice:Product:private] => 3
                    [category:Product:private] => meat
                )

        )

)

Group Join
Using a group join you can get all the products that match a given category bundled as a sequence
REM. Goup join performs left outer joins, see the results

$q = from($categories)
        ->groupJoin(
                from($products), '$c ==> $c', '$p ==> $p->getCategory()', '($c, $p) ==> array(
                    "category" => $c,
                    "products" => $p
                )'
        )
        ->toListDeep();

Resultat

Array
(
    [0] => Array
        (
            [0] => cherry
            [1] => Array
                (
                )

        )

    [1] => Array
        (
            [0] => apple
            [1] => Array
                (
                )

        )

    [2] => Array
        (
            [0] => blueberry
            [1] => Array
                (
                )

        )

    [3] => Array
        (
            [0] => cheese
            [1] => Array
                (
                    [0] => Product Object
                        (
                            [id:Product:private] => 4
                            [unitsInStock:Product:private] => 6
                            [productName:Product:private] => Gorgonzola Telino
                            [unitPrice:Product:private] => 6
                            [category:Product:private] => cheese
                        )

                    [1] => Product Object
                        (
                            [id:Product:private] => 8
                            [unitsInStock:Product:private] => 2
                            [productName:Product:private] => Camembert Pierrot
                            [unitPrice:Product:private] => 7
                            [category:Product:private] => cheese
                        )

                )

        )

    [4] => Array
        (
            [0] => meat
            [1] => Array
                (
                    [0] => Product Object
                        (
                            [id:Product:private] => 1
                            [unitsInStock:Product:private] => 0
                            [productName:Product:private] => Chef Anton's Gumbo Mix
                            [unitPrice:Product:private] => 2
                            [category:Product:private] => meat
                        )

                    [1] => Product Object
                        (
                            [id:Product:private] => 2
                            [unitsInStock:Product:private] => 0
                            [productName:Product:private] => Alice Mutton
                            [unitPrice:Product:private] => 5
                            [category:Product:private] => meat
                        )

                    [2] => Product Object
                        (
                            [id:Product:private] => 3
                            [unitsInStock:Product:private] => 5
                            [productName:Product:private] => Rostbratwurst
                            [unitPrice:Product:private] => 3
                            [category:Product:private] => meat
                        )

                )

        )

YaLinqo par l’exemple – Cas pratiques

Pour finir de vous convaincre ou pas de l’utilité de la librairie nous allons explorer deux cas pratiques.

Introduction

Les deux cas fonctionnent sur le même principe de base, et je ne cherches pas à réutiliser le code montré ici ni l’utiliser dans une application réelle, le code est destiné à démontrer l’utilité de manipuler des données via la librairie YaLinqo. Il n’est pas exploitable dans le sens où l’on ne prendra pas en considération la consommation mémoire, le temps d’indexation, la consommation processeur, les erreurs d’accès, les nombreux avertissements, les -trop- bonnes pratiques et tout ce qui prendrai trop de temps pour un simple exemple. Après cet avertissement angoissant !, nous allons construire un indexe de certains types de fichiers multimédia, pour ne pas ré-indexer à chaque fois le disque, l’indexe sera sérialisé dans un fichier en mode optimisation du développeur, le code sera factorisé entre les deux exemples également en mode optimisation du développeur.

L’objectif du premier code sera de parcourir des répertoires à la recherche d’images de type JPG et de sortir quelques statistiques sur ces images. Pour quoi faire ? Simplement parce que par excès d’étourderie je réimporte régulièrement plusieurs fois les mêmes cartes mémoires d’appareils photos, je duplique des répertoires entiers d’images pour les retravailler et j’oublie des répertoires entiers dupliqués dans mon disque dur… Alors je veux avoir des informations sur le nombre d’images en double, combien de place elles occupent sur le disque dur, évaluer le travail à faire et combien d’espace disque il me faudrait pour faire une sauvegarde des images uniques, organisées par date de prise de vue et appareil photographique.

Le second code permettra comme le premier de parcourir des répertoires, mais cette fois ci à la recherche de fichiers musicaux. Pour quoi faire ? Je suis très consommateur de musique en ligne, et je souhaiterai avoir quelques statistiques et quelques informations sur la musique que j’ai achetée, donc à priori les artistes et les styles que j’aime, pour découvrir d’autres artistes et d’autres musiques similaires. A cette fin j’ai repéré un service en ligne avec une API REST qui permet d’interroger une base de données musicale de plusieurs manières afin de retourner des résultats pertinents.

Un socle de code commun

Pour ces exemples les dépendances sont limitées. Nous utiliserons le serveur embarqué de PHP qui peut être lancé depuis les dernières versions de PHP depuis le répertoire du projet, en ligne de commande. Par ex. sur le port 8077 :

php -S localhost:8077

Le code utilisera un espace de noms, et nous utiliserons composer pour installer le composant Symfony2 Finder, la librairie GetID3 et la librairie YaLinqo. Votre fichier composer.json devrait ressembler à cela :

{
    "require": {
        "james-heinrich/getid3": "^1.9",
        "symfony/finder": "^3.0",
        "athari/yalinqo": "~2.0",
    }
}

Après installation (voir les commandes composer require ou composer update), il vous suffira d’inclure le fichier de chargement automatique des classes en début de programme pour exploiter les librairies tierces depuis l’application.

<?php
namespace MediaDiscovery\MediaIndex;

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Finder\Finder;

Le composant Symfony2 Finder

Le composant Finder est très simple et l’on pourrait très bien s’en passer pour cet exemple ou le remplacer par un autre équivalent, son utilité est de simplifier la recherche de fichiers et répertoires grâce à une interface fluide très compréhensible, cette simplicité nous distrait moins de notre objectif.

Nous allons l’utiliser pour parcourir récursivement un répertoire du système de fichier à la recherche de fichiers dont le nom répond à l’opérateur de globalisation sur les extensions .jpg, .mp3 etc… les répertoires qui ne sont pas accessibles en lecture au processus seront ignorés.

Ce qui se résume aux instructions :

        $this->finder
                ->files()
                ->ignoreUnreadableDirs()
                ->in($dir)
                ->name("*.{{$extensions}}");

        foreach ($this->finder as $file)
...

La librairie GetID3 – Analyse de fichiers

Le plus important du travail d’indexation sera exécuté par une librairie tierce qui analyse un fichier donné pour construire une structure de données représentative. Le contenu de la structure de données n’est malheureusement pas prédictible simplement car il est variable en fonction extraites du fichier (comme la présence ou pas d’indications EXIF), et ce contenu peut être assez imposant. Pour consommer moins de mémoire vive et rendre la structure plus cohérente entre deux appels, nous ferons une extraction des champs qui nous intéressent.

Le contenu de la structure est consultable sur GitHub.

Pour l’analyse des fichiers, nous allons inclure la librairie dans un conteneur afin de l’isoler un peu. Notez qu’une partie de la librairie possède des méthodes statiques que l’on ne peut pas injecter en dépendances.

/**
 * Conteneur pour l'outil d'analyse de fichiers getID3
 */
class AnalyserWrapper
{

    private $analyzer;

    /**
     * Instancie la classe getID3 et paramètre le jeux de caractères
     */
    public function __construct()
    {
        $this->analyzer = new \getID3();
        $this->analyzer->encoding = 'UTF-8';
    }

    /**
     * Analyse un fichier au travers de la librairie getID3
     * 
     * @param string $file Chemin vers un fichier
     * @return array Structure de données variable
     */
    public function analyze($file)
    {
        $metadata = @$this->analyzer->analyze($file);
        \getid3_lib::CopyTagsToComments($metadata);     // Restructure les données pour améliorer la cohésion de la structure
        $metadata['md5'] = md5_file($file); // La somme de contrôle md5 est ajoutée à la structure pour vérification d'unicité des fichiers
        return $metadata;
    }

}

La partie du code qui va se charger de l’indexation sera commune aux deux exemples, il s’agit simplement d’une classe abstraite avec des méthodes pour parcourir un répertoire, extraire des données après une analyse par la classe GetID3, faire persister l’indexe ainsi créé. Les méthodes abstraites permettent d’indiquer quelles extensions de fichier parcourir, de déterminer si les données d’analyse d’un fichier sont exploitables, et d’extraire les informations pertinentes d’une analyse.

/**
 * Méthodes communes aux classes d'indexation
 */
abstract class Index
{

    private $analyzer;
    private $finder;
    protected $data = [];

    /**
     * Constructeur
     * 
     * @param \MediaDiscovery\MediaIndex\AnalyserWrapper $analyzer Conteneur pour l'outil d'analyse de fichiers getID3
     * @param Finder $finder Composant Symfony de recherche de fichiers et répertoires
     */
    public function __construct(AnalyserWrapper $analyzer, Finder $finder)
    {
        $this->analyzer = $analyzer;
        $this->finder = $finder;
    }

    /**
     * Parcours un répertoire pour analysee les fichiers média
     * 
     * @param string $dir Chemin vers un répertoire
     * @return \MediaDiscovery\MediaIndex\Index Interface fluide
     */
    public function scan($dir)
    {
        $this->finder
                ->files()
                ->ignoreUnreadableDirs()
                ->in($dir)
                ->name("*.{{$this->getFilesExtensions()}}");

        foreach ($this->finder as $file)
            if (!empty($file->getRealpath()))
                $this->postAnalyze($this->analyzer->analyze($file->getRealpath()));

        return $this;
    }

    /**
     * Aggrège les données dans l'indexe après analyse d'un fichier
     * 
     * @param array $metadata Structure de données variable
     */
    private function postAnalyze($metadata)
    {
        if ($this->isUsable($metadata))
            $this->data [] = $this->extract($metadata);
    }

    /**
     * Retourne la liste des extensions de fichier à analyser
     */
    abstract function getFilesExtensions();

    /**
     * Détermine si les données d'analyse sont exploitables
     */
    abstract function isUsable($metadata);

    /**
     * Extrait un sous ensemble des données d'analyse pertinentes
     */
    abstract function extract($metadata);

    /**
     * Sérialise l'indexe dans un fichier pour assurer la persistence des données (au plus simple)
     * 
     * @param string $filename Nom de fichier
     * @return \MediaDiscovery\MediaIndex\Index Interface fluide
     */
    public function persist($filename)
    {
        file_put_contents(sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename, serialize($this->data));
        return $this;
    }

    /**
     * Désérialise l'indexe depuis un fichier
     * 
     * @param string $filename Nom de fichier
     * @return \MediaDiscovery\MediaIndex\Index Interface fluide
     */
    public function load($filename)
    {
        $data = unserialize(@file_get_contents(sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename));
        if ($data)
        {
            $this->data = $data;
            return true;
        }
        return false;
    }

}

A partir de là on peut exploiter l’indexe pour obtenir des informations dont le seul but est de démontrer l’emploi de la librairie YaLinqo au travers de requêtes sur la structure de données.

class PhotoIndex extends Index
{

    function getFilesExtensions()
    {
        return 'jpg';
    }
    
    function isUsable($metadata) {
        return true; // il fallait au moins tout ça...
    }
    
    function extract($metadata)
    {
        return [
            "size"      => $metadata["filesize"],
            "path"      => $metadata["filepath"],
            "name"      => $metadata["filename"],
            "md5"       => $metadata["md5"],
            "width"     => isset($metadata["video"]["resolution_x"])
                            ? $metadata["video"]["resolution_x"] 
                            : "",
            "height"    => isset($metadata["video"]["resolution_y"])
                            ? $metadata["video"]["resolution_y"] 
                            : "",
            "camera"    => isset($metadata["jpg"]["exif"]["IFD0"]["Model"]) 
                            ? $metadata["jpg"]["exif"]["IFD0"]["Model"]
                            : "",
            "filectime" => filectime($metadata["filepath"] . DIRECTORY_SEPARATOR . $metadata["filename"]),
            "datetime"  => isset($metadata["jpg"]["exif"]["IFD0"]["DateTime"])
                            ? $metadata["jpg"]["exif"]["IFD0"]["DateTime"]
                            : "",
        ];
    }

    function getDuplicatesInfos() {
...
    }
    
}

On pourra appeler simplement l’indexation par :

set_time_limit(0);//25 * 60);

$finder = new Finder();
$analyzer = new AnalyserWrapper();


$photoindex = new PhotoIndex($analyzer, $finder);

if (true || !$photoindex->load('iphotos'))  // a true si on veut forcer la réindexation
    $photoindex
            ->scan('C:')
            ->scan('D:\Media')
            ->persist('iphotos');

header('Content-Type: text/plain; charset=UTF-8');

var_dump($photoindex->getDuplicatesInfos());

Bien… tout ce code pour arriver au moment crucial de remplir la méthode getDuplicatesInfos() – ou d’autres- avec des requêtes qui s’avèrent très pratiques. Par ex.

    function getDuplicatesInfos() {
        
        $duplicates = @from($this->data)
                ->groupBy('$d ==> $d[md5]')
                ->where('$v, $k ==> count($v) > 1')
                ->select('$v, $k ==> ['
                        . '"md5" => $k, '
                        . '"count" => count($v), '
                        . '"images" => from($v)->orderBy(\'$i ==> $i["filectime"]\')->toList(), '
                        . ']')
                ->toList();
        
        $size1 = @from($this->data)->sum('$a ==> $a["size"]');
        $size2 = from($duplicates)->sum('$a ==> $a["images"][0]["size"] * ($a["count"] - 1)');

        return [
            "Nombre d'images : " => @from($this->data)->count(),
            "Espace occupé par les images sur le disque dur : " => $size1 / 1000000000 . ' Go',
            "Nombre d'images en double : " => from($duplicates)->sum('$a ==> $a["count"] - 1'),
            "Espace occupé par les images en double sur le disque dur : " => $size2 / 1000000000 . ' Go',
        ];
        
    }

Cela fait beaucoup de code pour peu d’exemples, mais prochainement j’ajouterai de plus nombreux exemples sur le deuxième cas pratique où la structure de données est plus complexe et les statistiques plus exploitables.