diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 000000000..8852b0a42 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,16 @@ +.row-even .line-block, .row-odd .line-block { + margin-left: 0; +} + +.versionmodified { + font-weight: bold; +} + +.wy-menu-vertical p.caption { + color: #b3b3b3; + margin-top: 16px; + margin-bottom: 0; +} +.wy-menu-vertical p.caption .caption-text { + font-size: 120%; +} \ No newline at end of file diff --git a/docs/_static/highlight.css b/docs/_static/highlight.css new file mode 100644 index 000000000..5dc9865ed --- /dev/null +++ b/docs/_static/highlight.css @@ -0,0 +1,59 @@ +div.highlight { + background-color: #343131 !important; +} + +div.highlight pre { + border: none; + color: white !important; +} + +div.highlight pre span.n, +div.highlight pre span.na, +div.highlight pre span.nb, +div.highlight pre span.nc, +div.highlight pre span.nf, +div.highlight pre span.nx, +div.highlight pre span.kn { + color: white; +} + +div.highlight pre span.nv { + color: #6ab0de +} + +div.highlight pre span.k, div.highlight pre span.o { + color: #ff8400; +} + +div.highlight pre span.mi, +div.highlight pre span.s, +div.highlight pre span.s1, +div.highlight pre span.s2, +div.highlight pre span.sr { + color: #56db3a; +} + +div.highlight pre span.hll { + background-color: #848484; +} + +div.highlight pre span.hll span.c1 { + color: #d8d8d8; +} + +div.highlight pre span.p { + color: #b3b3b3; +} + +table.highlighttable td { + padding: 0; +} + +table.highlighttable td div.linenodiv { + text-align: right; + width: 38px; +} + +div.highlight-json pre span.nt { + color: cornsilk; +} \ No newline at end of file diff --git a/docs/assets/img/psrcas-logo.png b/docs/assets/img/psrcas-logo.png new file mode 100644 index 000000000..9572c16c8 Binary files /dev/null and b/docs/assets/img/psrcas-logo.png differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..25ffb42ce --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex +from sphinx.highlighting import lexers +from pygments.lexers.web import PhpLexer + +lexers['php'] = PhpLexer(startinline=True, linenos=1) + +primary_domain = 'php' +highlight_language = 'php' + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# Warnings +suppress_warnings = ["image.nonlocal_uri"] + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Collection' +copyright = u'2019, Pol Dellaiera' +author = u'Pol Dellaiera' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'1.0.0' +# The full version, including alpha/beta/rc tags. +release = u'1.0.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = 'favicon.ico' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + + +def setup(app): + app.add_stylesheet('custom.css') + app.add_stylesheet('highlight.css') + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'CollectionDoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'drupol-collection-documentation.tex', u'drupol/collection Documentation', + u'Pol Dellaiera', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'drupol-collection', u'Collection Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'drupol-collection', u'Drupol/Collection Documentation', + author, 'Collection', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..b0754c37f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,62 @@ +Collection +========== + +Collection is a functional utility library for PHP. + +It's similar to `other available collection libraries`_ based on regular PHP arrays, +but with a lazy mechanism under the hood that strives to do as little work as possible while being as flexible +as possible. + +Collection leverages PHP's generators and iterators to allow you to work with very large data sets while keeping memory +usage as low as possible. + +For example, imagine your application needs to process a multi-gigabyte log file while taking advantage of this +library's methods to parse the logs. +Instead of reading the entire file into memory at once, this library may be used to keep only a small part of the file +in memory at a given time. + +On top of this, this library: + * is `immutable`_, + * is extendable, + * leverages the power of PHP `generators`_ and `iterators`_, + * uses `S.O.L.I.D. principles`_, + * doesn't depends or require any other library or framework. + +Except a few methods, most of methods are `pure`_ and return a new Collection object. + +This library has been inspired by the `Laravel Support Package`_ and `Lazy.js`_. + +It uses the following `PHP Standards Recommendations`_ : + +- `PSR-4`_ for classes autoloading, +- `PSR-12`_ for coding standards, + +This library is framework agnostic and can be integrated in any PHP project, in any framework. + +.. _Lazy.js: http://danieltao.com/lazy.js/ +.. _Laravel Support Package: https://github.com/illuminate/support +.. _pure: https://en.wikipedia.org/wiki/Pure_function +.. _S.O.L.I.D. principles: https://en.wikipedia.org/wiki/SOLID +.. _iterators: https://www.php.net/manual/en/class.iterator.php +.. _generators: https://www.php.net/manual/en/class.generator.php +.. _immutable: https://en.wikipedia.org/wiki/Immutable_object +.. _other available collection libraries: https://packagist.org/?query=collection +.. _PHP Standards Recommendations: https://www.php-fig.org/ +.. _PSR-4: https://www.php-fig.org/psr/psr-4/ +.. _PSR-12: https://www.php-fig.org/psr/psr-12/ + +.. toctree:: + :hidden: + + Collection + +.. toctree:: + :hidden: + :caption: Table of Contents + + Requirements + Installation + Usage + Tests + Contributing + Development diff --git a/docs/pages/contributing.rst b/docs/pages/contributing.rst new file mode 100644 index 000000000..d855f546a --- /dev/null +++ b/docs/pages/contributing.rst @@ -0,0 +1,7 @@ +Contributing +============ + +See the file `CONTRIBUTING.md`_ but feel free to contribute to this +library by sending Github pull requests. + +.. _CONTRIBUTING.md: .github/CONTRIBUTING.md diff --git a/docs/pages/development.rst b/docs/pages/development.rst new file mode 100644 index 000000000..ba67c43f5 --- /dev/null +++ b/docs/pages/development.rst @@ -0,0 +1,4 @@ +.. _development: + +Development +=========== diff --git a/docs/pages/installation.rst b/docs/pages/installation.rst new file mode 100644 index 000000000..f90b15924 --- /dev/null +++ b/docs/pages/installation.rst @@ -0,0 +1,10 @@ +Installation +============ + +The easiest way to install it is through Composer_ + +.. code-block:: bash + + composer require drupol/collection + +.. _Composer: https://getcomposer.org \ No newline at end of file diff --git a/docs/pages/requirements.rst b/docs/pages/requirements.rst new file mode 100644 index 000000000..256320208 --- /dev/null +++ b/docs/pages/requirements.rst @@ -0,0 +1,8 @@ +Requirements +============ + +PHP +--- + +PHP greater than 7.1 is required for this library. + diff --git a/docs/pages/tests.rst b/docs/pages/tests.rst new file mode 100644 index 000000000..569e2644b --- /dev/null +++ b/docs/pages/tests.rst @@ -0,0 +1,58 @@ +Tests, code quality and code style +================================== + +Every time changes are introduced into the library, `Github Actions`_ +run the tests. + +Tests are written with `PHPSpec`_. + +`PHPInfection`_ is also triggered used to ensure that your code is properly +tested. + +The code style is based on `PSR-12`_ plus a set of custom rules. +Find more about the code style in use in the package `drupol/php-conventions`_. + +A PHP quality tool, Grumphp_, is used to orchestrate all these tasks at each commit +on the local machine, but also on the continuous integration tools. + +To run the whole tests tasks locally, do + +.. code-block:: bash + + composer grumphp + +or + +.. code-block:: bash + + ./vendor/bin/grumphp run + +Here's an example of output that shows all the tasks that are setup in Grumphp and that +will check your code + +.. code-block:: bash + + $ ./vendor/bin/grumphp run + GrumPHP is sniffing your code! + Running task 1/13: SecurityChecker... ✔ + Running task 2/13: Composer... ✔ + Running task 3/13: ComposerNormalize... ✔ + Running task 4/13: YamlLint... ✔ + Running task 5/13: JsonLint... ✔ + Running task 6/13: PhpLint... ✔ + Running task 7/13: TwigCs... ✔ + Running task 8/13: PhpCsAutoFixerV2... ✔ + Running task 9/13: PhpCsFixerV2... ✔ + Running task 10/13: Phpcs... ✔ + Running task 11/13: PhpStan... ✔ + Running task 12/13: Phpspec... ✔ + Running task 13/13: Infection... ✔ + $ + + +.. _PSR-12: https://www.php-fig.org/psr/psr-12/ +.. _drupol/php-conventions: https://github.com/drupol/php-conventions +.. _Github Actions: https://github.com/drupol/collection/actions +.. _PHPSpec: http://www.phpspec.net/ +.. _PHPInfection: https://github.com/infection/infection +.. _Grumphp: https://github.com/phpro/grumphp \ No newline at end of file diff --git a/docs/pages/usage.rst b/docs/pages/usage.rst new file mode 100644 index 000000000..17801490c --- /dev/null +++ b/docs/pages/usage.rst @@ -0,0 +1,272 @@ +Usage +===== + +.. code-block:: php + + all(); // ['A', 'B', 'C', 'D', 'E'] + + // Get the first item. + $collection + ->first(); // A + + // Append items. + $collection + ->append('F', 'G', 'H') + ->all(); // ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + + // Prepend items. + $collection + ->prepend('1', '2', '3') + ->all(); // ['1', '2', '3', 'A', 'B', 'C', 'D', 'E'] + + // Split a collection into chunks of a given size. + $collection + ->chunk(2) + ->map(static function (Collection $collection) {return $collection->all();}) + ->all(); // [['A', 'B'], ['C', 'D'], ['E']] + + // Merge items. + $collection + ->merge([1, 2], [3, 4], [5, 6]) + ->all(); // ['A', 'B', 'C', 'D', 'E', 1, 2, 3, 4, 5, 6] + + // Map data + $collection + ->map( + static function ($value, $key) { + return sprintf('%s.%s', $value, $value); + } + ) + ->all(); // ['A.A', 'B.B', 'C.C', 'D.D', 'E.E'] + + // ::map() and ::walk() are not the same. + Collection::with(['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E']) + ->map( + static function ($value, $key) { + return strtolower($value); + } + ) + ->all(); // [0 => 'a', 1 => 'b', 2 => 'c', 3 = >'d', 4 => 'e'] + + Collection::with(['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E']) + ->walk( + static function ($value, $key) { + return strtolower($value); + } + ) + ->all(); // ['A' => 'a', B => 'b', 'C' => 'c', 'D' = >'d', 'E' => 'e'] + + // Tail + Collection::with(range('a', 'z')) + ->tail(3) + ->all(); // [23 => 'x', 24 => 'y', 25 => 'z'] + + // Reverse + Collection::with(range('a', 'z')) + ->tail(4) + ->reverse() + ->all(); // [25 => 'z', 24 => 'y', 23 => 'x', 22 => 'w'] + + // Flip operation. + // array_flip() can be used in PHP to remove duplicates from an array.(dedup-licate an array) + // See: https://www.php.net/manual/en/function.array-flip.php + // Example: + // $dedupArray = array_flip(array_flip(['a', 'b', 'c', 'd', 'a'])); // ['a', 'b', 'c', 'd'] + // However, in drupol/collection it doesn't behave as such. + // As this library is based on PHP Generators, it's able to return multiple times the same key when iterating. + // You end up with the following result when issuing twice the ::flip() operation. + Collection::with(['a', 'b', 'c', 'd', 'a']) + ->flip() + ->flip() + ->all(); // ['a', 'b', 'c', 'd', 'a'] + + // Infinitely loop over numbers, cube them, filter those that are not divisible by 5, take the first 100 of them. + Collection::range(0, INF) + ->map( + static function ($value, $key) { + return $value ** 3; + } + ) + ->filter( + static function ($value, $key) { + return $value % 5; + } + ) + ->limit(100) + ->all(); // [1, 8, 27, ..., 1815848, 1860867, 1906624] + + // Apply a callback to the values without altering the original object. + // If the callback returns false, then it will stop. + Collection::with(range('A', 'Z')) + ->apply( + static function ($value, $key) { + echo strtolower($value); + } + ); + + // Generate 300 distinct random numbers between 0 and 1000 + $random = static function() { + return mt_rand() / mt_getrandmax(); + }; + + Collection::iterate($random) + ->map( + static function ($value) { + return floor($value * 1000) + 1; + } + ) + ->distinct() + ->limit(300) + ->normalize() + ->all(); + + // The famous Fibonacci example: + Collection::with( + static function($start = 0, $inc = 1) { + yield $start; + + while(true) + { + $inc = $start + $inc; + $start = $inc - $start; + yield $start; + } + } + ) + ->limit(10) + ->all(); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] + + // Fibonacci using the static method ::iterate() + Collection::iterate( + static function($previous, $next) { + return [$next, $previous + $next]; + }, + 1,1 + ) + // Get the first item of each result. + ->pluck(0) + // Limit the amount of results to 10. + ->limit(10) + // Convert to regular array. + ->all(); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + + // Find the Golden Ratio with Fibonacci + $fibonacci = Collection::iterate( + static function($previous, $next) { + return [$next, $previous + $next]; + }, + 1,1 + ); + + Collection::with($fibonacci) + ->map( + static function(array $value, $key) { + [$previous, $next] = $value; + + return $next / $previous; + } + ) + ->limit(100) + ->last(); // 1.6180339887499 + + // Use an existing Generator as input data. + $readFileLineByLine = static function (string $filepath): Generator { + $fh = \fopen($filepath, 'rb'); + + while (false !== $line = fgets($fh)) { + yield $line; + } + + \fclose($fh); + }; + + $hugeFile = __DIR__ . '/vendor/composer/autoload_static.php'; + + Collection::with($readFileLineByLine($hugeFile)) + // Add the line number at the end of the line, as comment. + ->map( + static function ($value, $key) { + return str_replace(PHP_EOL, ' // line ' . $key . PHP_EOL, $value); + } + ) + // Find public static fields or methods among the results. + ->filter( + static function ($value, $key) { + return false !== strpos(trim($value), 'public static'); + } + ) + // Skip the first result. + ->skip(1) + // Limit to 3 results only. + ->limit(3) + // Implode into a string. + ->implode(); + + // Load a string + $string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Quisque feugiat tincidunt sodales. + Donec ut laoreet lectus, quis mollis nisl. + Aliquam maximus, orci vel placerat dapibus, libero erat aliquet nibh, nec imperdiet felis dui quis est. + Vestibulum non ante sit amet neque tincidunt porta et sit amet neque. + In a tempor ipsum. Duis scelerisque libero sit amet enim pretium pulvinar. + Duis vitae lorem convallis, egestas mauris at, sollicitudin sem. + Fusce molestie rutrum faucibus.'; + + // By default will have the same behavior as str_split(). + Collection::with($string) + ->explode(' ') + ->count(); // 71 + + // Or add a separator if needed, same behavior as explode(). + Collection::with($string, ',') + ->count(); // 9 + + // The Collatz conjecture (https://en.wikipedia.org/wiki/Collatz_conjecture) + $collatz = static function (int $value): int + { + return 0 === $value % 2 ? + $value / 2: + $value * 3 + 1; + }; + + Collection::iterate($collatz, 10) + ->until(static function ($number): bool { + return 1 === $number; + }) + ->all(); // [5, 16, 8, 4, 2, 1] + + // Regular values normalization. + Collection::with([0, 2, 4, 6, 8, 10]) + ->scale(0, 10) + ->all(); // [0, 0.2, 0.4, 0.6, 0.8, 1] + + // Logarithmic values normalization. + Collection::with([0, 2, 4, 6, 8, 10]) + ->scale(0, 10, 5, 15, 3) + ->all(); // [5, 8.01, 11.02, 12.78, 14.03, 15] + + // Fun with function convergence. + // Iterator over the function: f(x) = r * x * (1-x) + // Change that parameter $r to see different behavior. + // More on this: https://en.wikipedia.org/wiki/Logistic_map + $function = static function ($x = .3, $r = 2) { + return $r * $x * (1 - $x); + }; + + Collection::iterate($function) + ->map(static function ($value) {return round($value,2);}) + ->limit(10) + ->all(); // [0.42, 0.48, 0.49, 0.49, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]