├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── cover-data-block.png ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── rector.php └── src ├── Block.php ├── Enums └── Operator.php └── Traits ├── EditableBlock.php ├── ExportableBlock.php ├── FormattableBlock.php ├── IteratableBlock.php ├── LoadableBlock.php ├── QueryableBlock.php ├── TypeableBlock.php └── ValidableBlock.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.2 - 2025-10-05 4 | - Adding `getIntStrict()` method for returning strict int. 5 | - Adding `getStringStrict()` method for returning strict string. 6 | 7 | ## 1.0.1 - 2025-10-04 8 | - Adding `getInt()` method for returing casted integer value. 9 | - Adding `getBooleanStrict()` method for returning strict boolean. 10 | 11 | ## 1.0.0 - 2025-06-13 12 | - Upgrade dev package PestPHP 3 13 | - Upgrade dev package PHPstan 2 14 | - Upgrade dev package Rector 2 15 | 16 | ## 0.4.4 - 2025-03-22 17 | - Adding `getBoolean()` method for returning boolean value 18 | 19 | ## 0.4.3 - 2024-11-20 20 | - Using Operator class in the tests 21 | - Updating readme with 2 use cases 22 | - Adding rector to composer scripts 23 | - Welcome PHP 8.4 (adding GitHub actions configuration for PHP 8.4) 24 | 25 | ## 0.4.2 - 2024-10-15 26 | - Implementing the `groupByFunction()` to group elements based on a specific function. 27 | - Implementing `getString()` for returning a string 28 | - Implementing Operator Class with constants like Operator::EQUAL, Operator::GREATER_THAN ... 29 | - Implementing LIKE Operator 30 | 31 | ## 0.4.1 - 2024-10-11 32 | - Implementing `getFormattedByte()` for getting value as formatted byte 33 | 34 | ## 0.4.0 - 2024-10-11 35 | - `in` and `has` operator 36 | - Implementing a Trait for formatting data 37 | - Implementing getFormattedDateTime for getting value as formatted data 38 | 39 | ## 0.3.10 - 2024-10-07 40 | - Implementing the `saveToJson()` method 41 | 42 | ## 0.3.9 - 2024-08-08 43 | - Implementing the `exists()` method 44 | 45 | ## 0.3.8 - 2024-08-03 46 | - Implementing the `applyField()` method 47 | 48 | ## 0.3.7 - 2024-07-28 49 | - Implementing the `forEach()` method 50 | 51 | ## 0.3.6 - 2024-07-11 52 | - Implementing the `groupBy()` method 53 | 54 | ## 0.3.5 - 2024-07-07 55 | - Fix returning nested array type after `where()` 56 | 57 | ## 0.3.4 - 2024-07-07 58 | - Add `orderBy` for nestable attributes 59 | - Add filtering for array values (`in` parameter for `where()` method) 60 | - Add `dump()` and `dumpJson()` method to Block class 61 | 62 | 63 | ## 0.3.3 - 2024-06-29 64 | - Add `append()` for elements of Block and array 65 | - Add `appendItem()` for adding a single element to a Block object 66 | 67 | ## 0.3.2 - 2024-06-28 68 | - Add `fromJsonUrl()` method for loading Block data from a remote JSON (like APIs) 69 | - Add `like` operator for `where()` method, so you can filter for a substring 70 | - add more tests 71 | 72 | ## 0.3.1 - 2024-06-26 73 | - Add `has()` and `hasKey()` method 74 | 75 | ## 0.3.0 - 2024-06-24 76 | - Add validation with JSON Schema https://json-schema.org/ 77 | 78 | ## 0.2.0 - 2024-06-22 79 | - Add Yaml capabilities (loading from Yaml and exporting to Yaml) 80 | 81 | ## 0.1.1 - 2024-06-22 82 | - Add toJson() method for exporting JSON string 83 | 84 | ## 0.1.0 - 2024-06-21 85 | - Cleaning and refactoring the behavior of returning Block or native array in loops 86 | 87 | ## 0.0.6 - 2024-06-19 88 | - Add the `iterateBlock()` method, which allows to return of current elements as Block while looping. 89 | - Improve the documentation and the unit tests for "query" methods like `select()`, `where()` and `orderBy()` 90 | 91 | ## 0.0.5 - 2024-06-17 92 | - `select()` method for selecting fields 93 | 94 | ## 0.0.4 - 2024-06-16 95 | - `where()` method supports operators 96 | - `orderBy()` method allows you to sort Block data (ascending and descending order) 97 | 98 | ## 0.0.3 - 2024-06-16 99 | - `toArray()` for exporting data into an array (associative and nested array) 100 | 101 | ## 0.0.2 - 2024-06-15 102 | Fine-tuning initial release 103 | 104 | ## 0.0.1 - 2024-06-15 105 | Initial release: 106 | - `make`: static method for initializing the object from a nested array 107 | - `fromJsonString`: static method for initializing the object from a JSON file 108 | - `get`: method for retrieving elements on a nested key 109 | - `getBlock`: method for retrieving Block on a nested key 110 | - `set`: method for setting elements on a nested key 111 | - `where`: method for filtering data 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Commitment 4 | 5 | We, as members, contributors, and leaders, are committed to ensuring that participation in our community is a positive and respectful experience for everyone, regardless of their age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | Our goal is to create an open, welcoming, diverse, inclusive, and healthy community where everyone feels valued and respected. 8 | 9 | ## Expectations for Behavior 10 | To maintain a positive community environment, we expect all members, contributors, and leaders to adhere to the following guidelines: 11 | 12 | * **Empathy and Kindness**: Treat others with empathy and kindness. Show understanding and consideration towards fellow community members. 13 | 14 | * **Respect for Diverse Perspectives**: Be respectful of differing opinions, viewpoints, and experiences. Acknowledge that diversity of thought enriches our community. 15 | 16 | * **Constructive Feedback**: Constructively give feedback and be open to receiving it. Take responsibility for your actions and apologize when necessary, using the experience as an opportunity to learn. 17 | 18 | * **Community-Centered Focus**: Prioritize the well-being of the entire community, not just individual interests. Strive for what benefits the community as a whole. 19 | 20 | ## Unacceptable Behavior 21 | 22 | The following behaviors are not tolerated within our community: 23 | 24 | * **Sexualized Language or Imagery**: Avoid using sexualized language or imagery and refrain from making sexual advances. 25 | 26 | * **Trolling and Insults**: Do not engage in trolling, insulting or derogatory comments, or personal or political attacks. 27 | 28 | * **Harassment**: Harassment, whether public or private, is not acceptable. Respect personal boundaries and avoid intrusive behavior. 29 | 30 | * **Sharing Private Information**: Do not publish others' private information, such as physical or email addresses, without their explicit permission. 31 | 32 | * **Inappropriate Conduct**: Refrain from any conduct that could be considered unprofessional in a professional setting. 33 | 34 | ## Responsibilities for Enforcement 35 | 36 | Community leaders are responsible for upholding and enforcing these standards of behavior. They will take appropriate and fair corrective action in response to any behavior that is deemed inappropriate, threatening, offensive, or harmful. 37 | 38 | Community leaders have the authority to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that do not align with this Code of Conduct. They will communicate the reasons for moderation decisions when necessary. 39 | 40 | ## Scope 41 | 42 | This Code of Conduct applies in all community spaces. It also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 43 | 44 | ## Reporting and Enforcement 45 | 46 | If you encounter abusive, harassing, or otherwise unacceptable behavior, please report it to the community leaders responsible for enforcement via email. All complaints will be promptly and fairly reviewed and investigated. 47 | 48 | Community leaders are obligated to respect the privacy and security of the reporter of any incident. 49 | 50 | ## Enforcement Guidelines 51 | 52 | Community leaders will follow these guidelines to determine the consequences for any actions that violate this Code of Conduct: 53 | 54 | ### 1. Correction 55 | 56 | **Community Impact**: Inappropriate language or other unprofessional behavior. 57 | 58 | **Consequence**: A private, written warning from community leaders, providing clarity about the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 59 | 60 | ### 2. Warning 61 | 62 | **Community Impact**: A violation through a single incident or series of actions. 63 | 64 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 65 | 66 | ### 3. Temporary Ban 67 | 68 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 69 | 70 | **Consequence**: A temporary ban from any form of interaction or public communication with the community for a specified period. No public or private interaction with the people involved, including unsolicited interactions with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 71 | 72 | ### 4. Permanent Ban 73 | 74 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 75 | 76 | **Consequence**: A permanent ban from any form of public interaction within the community. 77 | 78 | ## Attribution 79 | 80 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 81 | 82 | The Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 83 | 84 | [homepage]: https://www.contributor-covenant.org 85 | 86 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Your contributions are highly appreciated, and they will be duly recognized. 4 | 5 | Before you proceed to create an issue or a pull request, please take a moment to familiarize yourself with our contribution guide. 6 | 7 | ## Etiquette 8 | 9 | This project thrives on the spirit of open-source collaboration. Our maintainers dedicate their precious time to creating and upholding the source code, and they share it with the hope that it will benefit fellow developers. Let's ensure they don't bear the brunt of abuse or anger for their hard work. 10 | 11 | When raising issues or submitting pull requests, let's maintain a considerate and respectful tone. Our goal is to exemplify that developers are a courteous and collaborative community. 12 | 13 | The maintainers have the responsibility to evaluate the quality and compatibility of all contributions with the project. Every developer brings unique skills, strengths, and perspectives to the table. Please respect their decisions, even if your submission isn't integrated. 14 | 15 | ## Relevance 16 | 17 | Before proposing or submitting new features, consider whether they are genuinely beneficial to the broader user base. Open-source projects serve a diverse group of developers with varying needs. It's important to assess whether your feature is likely to be widely useful. 18 | 19 | ## Procedure 20 | 21 | ### Preliminary Steps Before Filing an Issue 22 | 23 | - Try to replicate the problem to ensure it's not an isolated occurrence. 24 | - Verify if your feature suggestion has already been addressed within the project. 25 | - Review the pull requests to make sure a solution for the bug isn't already underway. 26 | - Check the pull requests to confirm that the feature isn't already under development. 27 | 28 | ### Preparing Your Pull Request 29 | 30 | - Examine the codebase to prevent duplication of your proposed feature. 31 | - Check the pull requests to verify that another contributor hasn't already submitted the same feature or fix. 32 | 33 | ## How to Contribute 34 | 35 | ### Fork the Repository 36 | 37 | 1. **Fork the Repository**: 38 | - Go to the [data-block repository](https://github.com/Hi-Folks/data-block) on GitHub. 39 | - Click the "Fork" button at the top right of the page. This creates a copy of the repository under your GitHub account. 40 | 41 | 2. **Clone the Forked Repository**: 42 | - Open your terminal and clone the forked repository to your local machine. 43 | ```sh 44 | git clone https://github.com/YOUR-USERNAME/data-block.git 45 | ``` 46 | - Navigate to the project directory: 47 | ```sh 48 | cd data-block 49 | ``` 50 | 51 | ### Create a new branch 52 | 53 | 3. **Create a Branch locally** 54 | - It's important to create a new branch for your work to keep your changes organized and separate from the main codebase 55 | ```sh 56 | git checkout -b your-feature-branch 57 | ``` 58 | - Replace the `your-feature-branch` with a meaningful name that describes the feature you're adding or the bug you're fixing. You can use also folders like `feat/101-add-new-filter-operator`. My suggestion is to use the `feat/` folder for new features and to use the `fix/` folder for bug fixing. In the slug of the name of the branch, I suggest you set the number of the issue you want to solve as a prefix. For example, the `feat/101-add-new-filter-operator` name is a new feature implementation requested in issue number 101. 59 | 60 | ### Make your changes locally 61 | 62 | 4. **Make Your Changes** 63 | - Implement your changes in the new branch you created. Ensure your code follows the project's style guidelines and passes any tests. 64 | 65 | 5. **Commit Your Changes** 66 | - After making your changes, commit them with a descriptive commit message 67 | ```sh 68 | git add . 69 | git commit -m "Description of the feature or fix" 70 | ``` 71 | ### Push and Open a Pull Request 72 | 73 | 6. **Push Your Changes** 74 | - Push your changes to your forked repository on GitHub 75 | ```sh 76 | git push origin your-feature-branch 77 | ``` 78 | 79 | 7. **Open a Pull Request** 80 | - Go to the original [data-block repository](https://github.com/Hi-Folks/data-block) on GitHub 81 | - Click the "Compare & pull request" button next to your recently pushed branch 82 | - Fill out the pull request template with relevant information about the changes you made 83 | 84 | 8. **Address Feedback** 85 | - If the project maintainers request any changes or provide feedback, update your branch accordingly and push the changes to your fork. The pull request will automatically update. 86 | 87 | 88 | ## Suggestions for opening a Pull Request 89 | 90 | To maintain coding consistency, we adhere to the PER coding standard and use PHPStan for static code analysis. You can utilize the following command: 91 | 92 | ```bash 93 | composer all 94 | ``` 95 | This command encompasses: 96 | 97 | - PER Coding Standard checks employing Pint/PHP_CodeSniffer. 98 | - PHPStan analysis at level 9. 99 | - Execution of all tests from the `./tests/*` directory using PestPHP. 100 | 101 | We recommend running `composer all` before committing and creating a pull request. 102 | 103 | When working on a pull request, it is advisable to create a new branch that originates from the main branch. This branch can serve as the target branch when you submit your pull request to the original repository. 104 | 105 | For a high-quality pull request, please ensure that you: 106 | 107 | - Include tests as part of your patch. We cannot accept submissions lacking tests. 108 | - Document changes in behavior, keeping the README.md and other pertinent documentation up-to-date. 109 | - Respect our release cycle. We follow SemVer v2.0.0, and we cannot afford to randomly break public APIs. 110 | - Stick to one pull request per feature. Multiple changes should be presented through separate pull requests. 111 | - Provide a cohesive history. Each individual *commit* within your pull request should serve a meaningful purpose. If you have made several intermediary commits during development, please consolidate them before submission. 112 | 113 | Happy coding! 🚀 114 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Hi-Folks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PHP Data Block package 3 |

4 | 5 |

6 | Data Block Package 7 |

8 | 9 | 10 |

11 | 12 | Latest Version 13 | 14 | 15 | Total Downloads 16 | 17 |
18 | Packagist License 19 | Supported PHP Versions 20 | GitHub last commit 21 |
22 | Tests 23 |

24 | 25 | 26 |

27 | 28 | PHP Package for Managing Nested Data 29 | 30 |

31 | This PHP package provides classes and methods for easily managing, querying, filtering, and setting nested data structures. 32 | The PHP Data Block package offers a streamlined approach to handling nested data, whether you're working with complex JSON data, hierarchical configurations, or deeply nested arrays. 33 | 34 | ## What you can do with PHP Data Block 35 | 36 | For example, with PHP Data Block, you can retrieve complex JSON from an API and then filter, sort, and handle the data. 37 | For example, here you can: 38 | 39 | - Retrieve the list of the repository from GitHub 40 | - Order the repository by the number of stars received (ascending, starting from the repos with more stars) 41 | - Loop the results 42 | - Get values from elements 43 | 44 | ```php 45 | orderBy('stargazers_count', 'desc') 54 | ->forEach( 55 | function ($item) { 56 | echo $item->get('full_name').' : '; 57 | echo $item->get('stargazers_count').PHP_EOL; 58 | } 59 | ); 60 | ``` 61 | 62 | Then, you can do more, you can: 63 | - Extract only the fields you need using the `select()` method 64 | - Filter the elements with the `where()` method and using the `Operator` class 65 | 66 | Here is an example: 67 | 68 | ```php 69 | select('full_name', 'stargazers_count') 79 | ->where('stargazers_count', Operator::GREATER_THAN, 0) 80 | ->orderBy('stargazers_count', 'desc') 81 | ->forEach( 82 | function ($item) { 83 | echo $item->get('full_name').' : '; 84 | echo $item->get('stargazers_count').PHP_EOL; 85 | } 86 | ); 87 | ``` 88 | This is just an overview as an appetizer :) 89 | 90 | Now, let's explore the classes and methods PHP Data Block provides. 91 | 92 | One "core" element of the PHP Data Block package is the Block PHP class. 93 | 94 | ## The Block class 95 | 96 | The **Block** class offers comprehensive methods to create, manage, and access nested data structures. 97 | 98 | The **Block** class provides various methods, including: 99 | 100 | - Creating structures from Arrays, JSON, and YAML files. 101 | - Querying nested data with ease. 102 | - Filtering data based on specific criteria. 103 | - Setting and updating values within deeply nested structures. 104 | 105 | ## Installing and using the Block class 106 | 107 | For adding to your projects, the Block class with its methods and helpers, you can run the `composer require` command: 108 | 109 | ```php 110 | composer require hi-folks/data-block 111 | ``` 112 | 113 | > To support the development, you can "star" ⭐ the repository: https://github.com/Hi-Folks/data-block 114 | 115 | Then, in your PHP files, you can import the `HiFolks\DataType\Block` Namespace: 116 | 117 | ```php 118 | use HiFolks\DataType\Block; 119 | ``` 120 | 121 | ## Method for creating Block objects 122 | To show the capabilities of the following methods, I will use this nested associative array: 123 | 124 | ```php 125 | $fruitsArray = [ 126 | "avocado" => 127 | [ 128 | 'name' => 'Avocado', 129 | 'fruit' => '🥑', 130 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Avocado', 131 | 'color'=>'green', 132 | 'rating' => 8 133 | ], 134 | "apple" => 135 | [ 136 | 'name' => 'Apple', 137 | 'fruit' => '🍎', 138 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Apple', 139 | 'color' => 'red', 140 | 'rating' => 7 141 | ], 142 | "banana" => 143 | [ 144 | 'name' => 'Banana', 145 | 'fruit' => '🍌', 146 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Banana', 147 | 'color' => 'yellow', 148 | 'rating' => 8.5 149 | ], 150 | "cherry" => 151 | [ 152 | 'name' => 'Cherry', 153 | 'fruit' => '🍒', 154 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Cherry', 155 | 'color' => 'red', 156 | 'rating' => 9 157 | ], 158 | ]; 159 | ``` 160 | 161 | ### The static `make()` method 162 | With the static `make()` method, you can generate a Block object from an associative array: 163 | ```php 164 | $data = Block::make($fruitsArray); 165 | ``` 166 | The `$data` object is an instance of the `Block` class. 167 | 168 | In the case you want to initialize an empty `Block` object, you can call the `make()` method with no parameters: 169 | ```php 170 | $data = Block::make(); 171 | ``` 172 | Once you initialize the Block object, you can use its methods. 173 | 174 | ### The `get()` method 175 | The `get()` method supports keys/indexes with the dot (or custom) notation for retrieving values from nested arrays. 176 | It returns the original type of data. If you need to obtain a Block object, you should use the `getBlock()` method instead of `get()`. 177 | For example: 178 | 179 | ```php 180 | $data->get('avocado'); // returns an array 181 | $data->get('avocado.color'); // returns the string "green" 182 | ``` 183 | 184 | For example, with the `$fruitsArray` sample data, the `$data->get("avocado")` is: 185 | - an array; 186 | - has five elements; 187 | 188 | For example, the `$data->get("avocado.color")` is: 189 | - a string; 190 | - has the value "green"; 191 | 192 | The `$data->get("avocado.rating")` is: 193 | - a numeric; 194 | - specifically an integer; 195 | 196 | The `$data->get("banana.rating")` is: 197 | - a numeric; 198 | - specifically a float; 199 | 200 | 201 | You can customize the notation with a different character: 202 | 203 | ```php 204 | $data->get('apple#fruit', charNestedKey: '#'); // 🍎 205 | ``` 206 | 207 | If you are going to access a not valid key, a `null` value is returned: 208 | 209 | ```php 210 | $value = $data->get('apple.notexists'); // null 211 | ``` 212 | You can define a default value in the case the key doesn't exist: 213 | 214 | ```php 215 | $value = $data->get( 216 | 'apple.notexists', 217 | '🫠' 218 | ); // 🫠 219 | ``` 220 | 221 | And you can combine the default value and the nested character: 222 | ```php 223 | $value = $data->get( 224 | 'apple#notexists', 225 | '🫠', 226 | '#' 227 | ); // 🫠 228 | ``` 229 | 230 | ### The `getFormattedDateTime()` for getting and formatting a date-time field value 231 | 232 | When working with date-time fields, consider utilizing the `getFormattedDateTime()` method instead of relying solely on `get()`. This approach not only retrieves the value but also formats it according to the specified date-time format, as defined by the second optional parameter `$format`. 233 | 234 | By default, this formatting is set to "Y-m-d H:i:s", providing a convenient and standardized output. However, customizing the format allows for more flexibility in presenting your data. 235 | 236 | Here are some key points: 237 | 238 | * `getFormattedDateTime()` combines the functionality of `get()` with date-time formatting. 239 | * The second parameter `$format` controls the date-time format used for formatting. 240 | * Custom formats can be applied to provide tailored outputs for different use cases. 241 | 242 | ### The `getFormattedByte()` for getting and formatting a 'byte' field value 243 | 244 | The `getFormattedByte()` method retrieves and formats a byte value from a specified field in a data block. It converts the raw byte count into a more human-readable format (e.g., GB, MB, etc.), with an optional precision parameter to control the number of decimal places displayed. 245 | 246 | Parameters: 247 | - `$path` (string): The path to the field containing the byte value (e.g., "assets.0.total_bytes"). 248 | - `$precision` (int): (Optional) Number of decimal places to include in the formatted result. The default is 2. 249 | 250 | Example usage: 251 | 252 | ```php 253 | $data1->getFormattedByte("assets.0.total_bytes"); // Returns "5.98 GB" 254 | $data1->getFormattedByte("assets.1.total_bytes"); // Returns "2.18 GB" 255 | $data1->getFormattedByte("assets.1.total_bytes", 5); // Returns "2.18288 GB" 256 | $data1->getFormattedByte("assets.1.total_bytes", 0); // Returns "2 GB" 257 | ``` 258 | 259 | Key Features: 260 | - Automatic unit conversion: converts bytes into appropriate units (e.g., KB, MB, GB) based on the size. 261 | - Customizable precision: you can specify the number of decimal places for the output, making it flexible for various use cases. 262 | 263 | ### The `getString()` method 264 | 265 | The `getString()` method retrieves the value of a specified field as a string from a data block. If the field does not exist or is null, it returns a default value, which can be customized. 266 | 267 | Parameters: 268 | 269 | - `$path` (string): The path to the field (e.g., "0.commit.author.date"). 270 | - `$default` (string|null): (Optional) The default value to return if the field doesn't exist. Defaults to null. 271 | 272 | Example Usage: 273 | ```php 274 | $data1->getString("0.commit.author.date"); // Returns the field value as a string 275 | $data1->getString("0.commit.author.notexists"); // Returns null 276 | $data1->getString("0.commit.author.notexists", "AA"); // Returns "AA" 277 | $data1->getString("0.commit.comment_count"); // Returns "0" as a string even if the field value is an integer 278 | ``` 279 | 280 | ### The `getStringStrict()` method 281 | 282 | The `getStringStrict()` method retrieves the value of a specified field as a string from a data block. If the field does not exist or is null, it returns a default string value, which can be customized ("" by default). 283 | 284 | Parameters: 285 | 286 | - `$path` (string): The path to the field (e.g., "0.commit.author.date"). 287 | - `$default` (string): (Optional) The default value to return if the field doesn't exist. Defaults to "". 288 | 289 | Example Usage: 290 | ```php 291 | $data1->getStringStrict("0.commit.author.date"); // Returns the field value as a string 292 | $data1->getStringStrict("0.commit.author.notexists"); // Returns an empty string "" 293 | $data1->getStringStrict("0.commit.author.notexists", "AA"); // Returns "AA" 294 | $data1->getStringStrict("0.commit.comment_count"); // Returns "0" as a string even if the field value is an integer 295 | ``` 296 | 297 | ### The `getInt()` method 298 | 299 | The `getInt()` method retrieves the value of a specified field as a integer from a data block. If the field does not exist or is null, it returns a default value, which can be customized (null by default). 300 | Parameters: 301 | 302 | - `$path` (string): The path to the field (e.g., "0.author.id"). 303 | - `$default` (null|int): (Optional) The default value to return if the field doesn't exist. Defaults to null. 304 | - `$charNestedKey` (string): the character separator for nested field names. The default is ".". 305 | 306 | Example usage: 307 | ```php 308 | $data1->getInt("0.author.id"); // Returns the field value as an integer, for example 678434 309 | $data1->getInt("0.author.idx"); // Returns null because the field doesn't exists 310 | $data1->getInt("0.author.idx", 44); // Returns 44 because the field doesn't exists, and you set a default, in this case 44 311 | ``` 312 | 313 | ### The `getIntStrict()` method 314 | 315 | The `getIntStrict()` method retrieves the value of a specified field as a integer from a data block. If the field does not exist or is null, it returns a default value, which can be customized (0 by default). 316 | Parameters: 317 | 318 | - `$path` (string): The path to the field (e.g., "0.author.id"). 319 | - `$default` (int): (Optional) The default value to return if the field doesn't exist. Defaults to 0. 320 | - `$charNestedKey` (string): the character separator for nested field names. The default is ".". 321 | 322 | Example usage: 323 | ```php 324 | $data1->getIntStrict("0.author.id"); // Returns the field value as an integer, for example 678434 325 | $data1->getIntStrict("0.author.idx"); // Returns 0 because the field doesn't exists, and the method is strict 326 | $data1->getIntStrict("0.author.idx", 44); // Returns 44 because the field doesn't exists, and you set a default, in this case 44 327 | ``` 328 | 329 | ### The `getBoolean()` method 330 | 331 | The `getBoolean()` method retrieves the value of a specified field as a boolean from a data block. If the field does not exist or is null, it returns a default value, which can be customized (null by default). 332 | Parameters: 333 | 334 | - `$path` (string): The path to the field (e.g., "0.author.id"). 335 | - `$default` (null|bool): (Optional) The default value to return if the field doesn't exist. Defaults to null. 336 | - `$charNestedKey` (string): the character separator for nested field names. The default is ".". 337 | 338 | Example usage: 339 | ```php 340 | $data1->getBoolean("0.author.site_admin"); // Returns the field value as an boolean, for example true 341 | $data1->getBoolean("0.author.site_admin_notexists"); // Returns null because the field doesn't exists 342 | $data1->getBoolean("0.author.site_admin_notexists", true); // Returns true because the field doesn't exists, and you set a default, in this case true 343 | ``` 344 | 345 | ### The `getBooleanStrict()` method 346 | 347 | The `getBooleanStrict()` method retrieves the value of a specified field as a boolean from a data block. If the field does not exist or is null, it returns a strict boolean default value, which can be customized (false by default). 348 | Parameters: 349 | 350 | - `$path` (string): The path to the field (e.g., "0.author.id"). 351 | - `$default` (bool): (Optional) The default value to return if the field doesn't exist. Defaults to false. 352 | - `$charNestedKey` (string): the character separator for nested field names. The default is ".". 353 | 354 | Example usage: 355 | ```php 356 | $data1->getBooleanStrict("0.author.site_admin"); // Returns the field value as an boolean, for example true 357 | $data1->getBooleanStrict("0.author.site_admin_notexists"); // Returns false because the field doesn't exists 358 | $data1->getBooleanStrict("0.author.site_admin_notexists", true); // Returns true because the field doesn't exists, and you set a default, in this case true 359 | ``` 360 | 361 | 362 | ### The `getBlock()` method 363 | If you need to manage a complex array (nested array) or an array obtained from a complex JSON structure, you can access a portion of the array and obtain the `Block` object via the `getBlock()` method. 364 | 365 | Let's see an example: 366 | 367 | ```php 368 | $appleData = $data->getBlock("apple") 369 | // $data is the Block instance so that you can access 370 | // to the Block methods like count() 371 | $data->getBlock("apple")->count(); 372 | ``` 373 | 374 | If the element accessed via `getBlock()` is a scalar type (integer, float, string, etc.), a Block object (with just one element) will be returned using `getBlock()`. 375 | 376 | For example, `$data->getBlock("avocado")` returns a Block object with five elements. 377 | 378 | For example, `$data->getBlock("avocado.color")` returns a Block object with just one element. 379 | 380 | If you are going to access a non-valid key, an empty Block object is returned, so the `$data->getBlock("avocado.notexists")` returns a Block object with a length equal to 0. 381 | 382 | ### The `set()` method 383 | The `set()` method supports keys with the dot (or custom) notation for setting values for nested data. 384 | If a key doesn't exist, the `set()` method creates one and sets the value. 385 | If a key already exists, the `set()` method will replace the value related to the key. 386 | 387 | #### Parameters 388 | 389 | - `key` (int|string): The key to which the value should be assigned. If a string is provided, you can use dot notation to set nested values. 390 | - `value` (mixed): The value to be assigned to the specified key. 391 | - `charNestedKey` (string, optional): The character used for dot notation in nested keys. Defaults to `.`. 392 | 393 | #### Returns 394 | 395 | - `self`: Returns the instance of the class for method chaining. 396 | 397 | #### Example Usage 398 | 399 | ```php 400 | $articleText = "Some words as a sample sentence"; 401 | $textField = Block::make(); 402 | $textField->set("type", "doc"); 403 | $textField->set("content.0.content.0.text", $articleText); 404 | $textField->set("content.0.content.0.type", "text"); 405 | $textField->set("content.0.type", "paragraph"); 406 | ``` 407 | 408 | So when you try to set a nested key as "content.0.content.0.text", it will be created elements as a nested array. 409 | 410 | Once you set the values, you can access them via `get()` (or `getBlock()`) methods: 411 | 412 | ```php 413 | $textField->get("content.0.content.0.text"); 414 | ``` 415 | 416 | ### Extracting Keys 417 | 418 | Via the `keys()` method, you can retrieve the list of the keys: 419 | 420 | ```php 421 | $data = Block::make($fruitsArray); 422 | $keys = $data->keys(); 423 | /* 424 | Array 425 | ( 426 | [0] => avocado 427 | [1] => apple 428 | [2] => banana 429 | [3] => cherry 430 | ) 431 | */ 432 | ``` 433 | 434 | You can retrieve the keys of a nested element, combining the usage of `getBlock()` and the `keys()`: 435 | 436 | ```php 437 | $data = Block::make($fruitsArray); 438 | $keys = $data->getBlock("avocado")->keys(); 439 | 440 | /* 441 | Array 442 | ( 443 | [0] => name 444 | [1] => fruit 445 | [2] => wikipedia 446 | [3] => color 447 | [4] => rating 448 | ) 449 | */ 450 | ``` 451 | 452 | ## Exporting data 453 | 454 | ### Exporting to array with `toArray()` 455 | The `toArray()` method can access the native array (associative and nested). 456 | 457 | This is helpful when manipulating data with the Block class, and at a certain point, you need to send the data to your function or a function from a third-party package that expects to receive a native array as a parameter. 458 | 459 | ```php 460 | $file = "./composer.json"; 461 | $composerContent = Block::fromJsonFile($file); 462 | // here you can manage $composerContent with Block methods 463 | // end then exports the Block data into a native array 464 | $array = $composerContent->toArray(); 465 | ``` 466 | 467 | ### Exporting to JSON string with `toJson()` 468 | If you need to generate a valid JSON string using the content of the Block object, you can use the `toJson()` method. 469 | 470 | This is helpful when you are manipulating data with the Block class and at a certain point need to send the data in JSON string format to your own function or a function from a third-party package that expects to receive a JSON string as a parameter. 471 | 472 | ```php 473 | $data = Block::make($fruitsArray); 474 | $jsonString = $data->toJson(); // JSON string with "pretty print" 475 | ``` 476 | 477 | ### Exporting to YAML string with `toYaml()` 478 | If you need to generate a valid YAML string using the content of the Block object, you can use the `toYaml()` method. 479 | 480 | This is helpful when manipulating data with the Block class and, at a certain point, need to send the data in YAML string format to your function or a function from a third-party package that expects to receive a YAML string as a parameter. 481 | 482 | ```php 483 | $data = Block::make($fruitsArray); 484 | $yamlString = $data->toYaml(); // YAML string 485 | ``` 486 | 487 | ### Saving JSON to a file with `saveToJson()` 488 | If you need to save the JSON string in a file using the content of the Block object, you can use the `saveToJson()` method. 489 | 490 | This is helpful when you are manipulating data with the Block class and at a certain point need to save the data in JSON string format to a file. 491 | The `saveToJson()` method has two parameters: 492 | 493 | - `filename`: the first parameter (mandatory) with the filename; 494 | - `overwrite`: the second parameter (optional), If the file exists, the file is not saved by default, unless you set the overwrite parameter as true. 495 | 496 | ```php 497 | $data = Block::make($fruitsArray); 498 | $jsonString = $data->saveToJson('./fruits.json', true); 499 | ``` 500 | 501 | 502 | ## Loading Data 503 | 504 | ### Loading Data from JSON file 505 | 506 | ```php 507 | $file = "./composer.json"; 508 | $composerContent = Block::fromJsonFile($file); 509 | echo $composerContent->get("name"); // for example: "hi-folks/data-block" 510 | echo $composerContent->get("authors.0.name"); // for example: "Roberto B." 511 | ``` 512 | 513 | ### Loading Data from JSON URL 514 | 515 | You can build your Block data from a remote JSON (like an API). 516 | For example, you can use the `fromJsonUrl()` method to build a Block object from the latest commits via GitHub API. 517 | Retrieving JSON API into a Block object is useful for applying the methods provided by the Block class, for example, filtering the data. In the example, I'm going to filter the commit based on the name of the author of the commit: 518 | 519 | ```php 520 | $url = "https://api.github.com/repos/hi-folks/data-block/commits"; 521 | $commits = Block::fromJsonUrl($url); 522 | $myCommits = $commits->where("commit.author.name", Operator::LIKE, "Roberto"); 523 | foreach ($myCommits as $value) { 524 | echo $value->get("commit.message") . PHP_EOL; 525 | } 526 | ``` 527 | 528 | ### Loading Data from YAML file 529 | 530 | ```php 531 | $file = "./.github/workflows/run-tests.yml"; 532 | $workflow = Block::fromYamlFile($file); 533 | echo $workflow->get("name"); // Name of the GitHub Action Workflow 534 | echo $workflow->get("jobs.test.runs-on"); 535 | echo $workflow->get("on.0"); // push , the first event 536 | ``` 537 | 538 | ## Adding and appending elements 539 | 540 | ### Appending the elements of a Block object to another Block object 541 | If you have a Block object, you can add elements from another Block object. 542 | One use case is if you have multiple JSON files and want to retrieve paginated content from an API. In this case, you want to create one Block object with all the elements from every JSON file. 543 | 544 | ```php 545 | $data1 = Block::fromJsonFile("./data/commits-10-p1.json"); 546 | $data2 = Block::fromJsonFile("./data/commits-10-p2.json"); 547 | $data1->count(); // 10 548 | $data2->count(); // 10 549 | $data1->append($data2); 550 | $data1->count(); // 20 551 | $data2->count(); // 10 552 | ``` 553 | 554 | ### Appending the elements of an array to a Block object 555 | 556 | If you have an array, you can add elements to a Block object. 557 | Under the hood, a Block object is an array (that potentially can be a nested array). Appending an array will add elements at the root level: 558 | 559 | 560 | ```php 561 | $data1 = Block::make(["a","b"]); 562 | $arrayData2 = ["c","d"]; 563 | $data1->count(); // 2 564 | $data1->append($arrayData2); 565 | $data1->count(); // 4 566 | ``` 567 | 568 | ### Appending an element 569 | If you need to append an element as a single element (even if it is an array or a Block object), you can use the `appendItem()` function: 570 | 571 | ```php 572 | $data1 = Block::make(["a", "b"]); 573 | $arrayData2 = ["c", "d"]; 574 | $data1->appendItem($arrayData2); 575 | $data1->count(); // 3 because a, b, and the whole array c,d as single element 576 | $data1->toArray(); 577 | /* 578 | [ 579 | 'a', 580 | 'b', 581 | [ 582 | 'c', 583 | 'd', 584 | ], 585 | ] 586 | */ 587 | ``` 588 | 589 | ## Querying, sorting data 590 | 591 | ### The `where()` method 592 | You can filter data elements for a specific key with a specific value. 593 | You can also set the operator 594 | 595 | ```php 596 | $composerContent = Block::fromJsonString($jsonString); 597 | $banners = $composerContent->getBlock("story.content.body")->where( 598 | "component", 599 | Operator::EQUAL, 600 | "banner", 601 | ); 602 | ``` 603 | 604 | With the `where()` method, the filtered data keeps the original keys. 605 | If you want to avoid preserving the keys and set new integer keys starting from 0, you can set the fourth parameter (`preserveKeys`) as `false`. 606 | 607 | ```diff 608 | $composerContent = Block::fromJsonString($jsonString); 609 | $banners = $composerContent->getBlock("story.content.body")->where( 610 | "component", 611 | Operator::NOT_EQUAL, 612 | "banner", 613 | + false 614 | ); 615 | ``` 616 | 617 | With `where()` method you can use different operators, like "==", ">", "<" etc. 618 | 619 | 620 | 621 | You can use also the `has` operator in the case your nested data contains arrays or `in` operator in the case you want to check if your data field value is included in an array of elements. 622 | 623 | #### The operators 624 | 625 | The `Operator` class provides a set of predefined constants that represent comparison and logical operators. This ensures type safety and prevents errors from using invalid or misspelled operators in your data comparisons. 626 | 627 | Supported Operators: 628 | - `Operator::EQUAL` (==) 629 | - `Operator::GREATER_THAN` (>) 630 | - `Operator::LESS_THAN` (<) 631 | - `Operator::GREATER_THAN_OR_EQUAL` (>=) 632 | - `Operator::LESS_THAN_OR_EQUAL` (<=) 633 | - `Operator::NOT_EQUAL` (!=) 634 | - `Operator::STRICT_NOT_EQUAL` (!==) 635 | - `Operator::IN` (array inclusion) 636 | - `Operator::HAS` (array containment) 637 | - `Operator::LIKE` (string contains) 638 | 639 | > The `Operator` class is defined in the `use HiFolks\DataType\Enums\Operator` namespace. 640 | 641 | #### The `in` operator 642 | 643 | The `in` operator is used within the where method to filter elements from a data collection based on whether a specific field's value exists within a given array of values. 644 | The behavior is as follows: 645 | 646 | ```php 647 | $data->where("field", Operator::IN, ["value1", "value2", ...]) 648 | ``` 649 | 650 | If the field's value exists in the provided array, the element is included in the result. 651 | Example: Filtering fruits by color that match either "green" or "black" 652 | ```php 653 | $greenOrBlack = $data->where("color", Operator::IN, ["green", "black"]); 654 | ``` 655 | 656 | You should use the `in` operator if your field is a scalar type (for example string or number) and you need to check if it is included in a list of values (array). 657 | 658 | #### The `has` operator 659 | 660 | The `has` operator is used within the where method to filter elements from a data collection based on whether a specific field contains a given value, typically in cases where the field holds an array or a collection of tags or attributes. The behavior is as follows: 661 | 662 | ```php 663 | $data->where("field", Operator::HAS, "value") 664 | ``` 665 | 666 | For example if you have posts and each post can have multiple tags, you can filter posts with a specific tag: 667 | 668 | ```php 669 | $url = "https://dummyjson.com/posts"; 670 | $posts = Block 671 | ::fromJsonUrl($url) 672 | ->getBlock("posts"); 673 | 674 | $lovePosts = $posts->where("tags", Operator::HAS, "love"); 675 | ``` 676 | #### Summary `in` VS `has` 677 | 678 | The `in` operator filters elements by matching a field's value against an array of possible values. If the value exists in the array, the element is included in the result. An empty array returns no results. 679 | 680 | The `has` operator filters elements by checking if a specific value exists within a field (usually an array or a collection). If the value exists, the element is included in the result. Non-existent values return no matches. 681 | 682 | ### The `orderBy()` method 683 | You can order or sort data for a specific key. 684 | For example, if you want to retrieve the data at `story.content.body` key and sort them by `component` key: 685 | 686 | ```php 687 | $composerContent = Block::fromJsonString($jsonString); 688 | $bodyComponents = $composerContent->getBlock("story.content.body")->orderBy( 689 | "component", "asc" 690 | ); 691 | ``` 692 | 693 | You can also order data for a nested attribute. 694 | Consider retrieving a remote JSON like the dummy JSON posts and then ordering the posts via the `reactions.likes` nested field in descending order: 695 | 696 | ```php 697 | use HiFolks\DataType\Block; 698 | 699 | $posts = Block 700 | ::fromJsonUrl("https://dummyjson.com/posts") 701 | ->getBlock("posts"); 702 | echo $posts->count(); // 30 703 | $mostLikedPosts = $posts->orderBy("reactions.likes", "desc"); 704 | $mostLikedPosts->dump(); 705 | ``` 706 | 707 | ### The `select()` method 708 | The `select()` method allows you to select only the needed fields. 709 | You can list the field names you need as parameters for the `select()` method. 710 | For example: 711 | 712 | ```php 713 | use HiFolks\DataType\Block; 714 | $dataTable = [ 715 | ['product' => 'Desk', 'price' => 200, 'active' => true], 716 | ['product' => 'Chair', 'price' => 100, 'active' => true], 717 | ['product' => 'Door', 'price' => 300, 'active' => false], 718 | ['product' => 'Bookcase', 'price' => 150, 'active' => true], 719 | ['product' => 'Door', 'price' => 100, 'active' => true], 720 | ]; 721 | $table = Block::make($dataTable); 722 | $data = $table 723 | ->select('product' , 'price'); 724 | print_r($data->toArray()); 725 | ``` 726 | 727 | You can combine the `select()`, the `where()`, and the `orderBy()` method. 728 | If you want to retrieve elements with `product` and `price` keys, with a price greater than 100 and ordered by `price`: 729 | 730 | ```php 731 | $table = Block::make($dataTable); 732 | $data = $table 733 | ->select('product' , 'price') 734 | ->where('price', Operator::GREATER_THAN, 100) 735 | ->orderBy("price"); 736 | print_r($data->toArray()); 737 | /* 738 | Array 739 | ( 740 | [0] => Array 741 | ( 742 | [product] => Bookcase 743 | [price] => 150 744 | ) 745 | 746 | [1] => Array 747 | ( 748 | [product] => Desk 749 | [price] => 200 750 | ) 751 | 752 | [2] => Array 753 | ( 754 | [product] => Door 755 | [price] => 300 756 | ) 757 | 758 | ) 759 | */ 760 | ``` 761 | 762 | ### The `groupBy()` method 763 | 764 | Groups the elements of the `Block` object by a specified field. 765 | 766 | This method takes a field name as an argument and groups the elements of the `Block` object based on the values of that field. Each element is grouped into an associative array where the keys are the values of the specified field and the values are arrays of elements that share that key. 767 | 768 | ```php 769 | use HiFolks\DataType\Block; 770 | $data = Block::make([ 771 | ['type' => 'fruit', 'name' => 'apple'], 772 | ['type' => 'fruit', 'name' => 'banana'], 773 | ['type' => 'vegetable', 'name' => 'carrot'], 774 | ]); 775 | $grouped = $data->groupBy('type'); 776 | $grouped->dumpJson(); 777 | /* 778 | { 779 | "fruit": [ 780 | { 781 | "type": "fruit", 782 | "name": "apple" 783 | }, 784 | { 785 | "type": "fruit", 786 | "name": "banana" 787 | } 788 | ], 789 | "vegetable": [ 790 | { 791 | "type": "vegetable", 792 | "name": "carrot" 793 | } 794 | ] 795 | } 796 | */ 797 | ``` 798 | 799 | ### The `groupByFunction()` method 800 | 801 | The `groupByFunction()` method allows you to group items from an Block based on a grouping logic provided by a callback function (closure). The function returns an associative array where the keys represent groupings defined by the callback, and the values are arrays of elements that belong to each group. 802 | 803 | ```php 804 | $fruits = [ 805 | ['name' => 'Apple', 'type' => 'Citrus', 'quantity' => 15], 806 | ['name' => 'Banana', 'type' => 'Tropical', 'quantity' => 10], 807 | ['name' => 'Orange', 'type' => 'Citrus', 'quantity' => 8], 808 | ['name' => 'Mango', 'type' => 'Tropical', 'quantity' => 5], 809 | ['name' => 'Lemon', 'type' => 'Citrus', 'quantity' => 12] 810 | ]; 811 | $fruitsBlock = Block::make($fruits); 812 | $groupedByQuantityRange = $fruitsBlock->groupByFunction( 813 | fn($fruit): string => 814 | match (true) { 815 | $fruit['quantity'] < 10 => 'Low', 816 | $fruit['quantity'] < 15 => 'Medium', 817 | default => 'High', 818 | }, 819 | ); 820 | // It returns: 821 | /* 822 | { 823 | "High": [ 824 | { 825 | "name": "Apple", 826 | "type": "Citrus", 827 | "quantity": 15 828 | } 829 | ], 830 | "Medium": [ 831 | { 832 | "name": "Banana", 833 | "type": "Tropical", 834 | "quantity": 10 835 | }, 836 | { 837 | "name": "Lemon", 838 | "type": "Citrus", 839 | "quantity": 12 840 | } 841 | ], 842 | "Low": [ 843 | { 844 | "name": "Orange", 845 | "type": "Citrus", 846 | "quantity": 8 847 | }, 848 | { 849 | "name": "Mango", 850 | "type": "Tropical", 851 | "quantity": 5 852 | } 853 | ] 854 | } 855 | */ 856 | ``` 857 | 858 | ### The `exists()` method 859 | 860 | You can use the `exists()` method to check if an element that meets a certain condition exists. This method is a convenient way to determine if any records match your query without needing to count them explicitly. 861 | 862 | Here’s how you can use it: 863 | 864 | ```php 865 | $has = $composerContent 866 | ->getBlock("story.content.body")->where( 867 | "component", 868 | "banner", 869 | )->exists(); 870 | ``` 871 | 872 | This will return true if a banner component exists, and false if it does not. 873 | 874 | ## Looping Data 875 | The Block class implements the Iterator interface. 876 | While looping an array via Block, by default, if the current element should be an array, a Block is returned so that you can access the Block method for handling the current array item in the loop. 877 | For example, with the previous code, if you loop through `$data` (which is a Block object), each element in each iteration of the loop will be an array with two elements, with the keys `product` and `price`. 878 | If in the loop you need to manage the current element via Block class, you should manually call the `Block::make`, for example: 879 | 880 | ```php 881 | $table = Block::make($dataTable); 882 | foreach ($table as $key => $item) { 883 | echo $item->get("price"); 884 | } 885 | ``` 886 | 887 | You can apply filters and then loop into the result: 888 | 889 | ```php 890 | $table = Block::make($dataTable); 891 | $data = $table 892 | ->select('product', 'price') 893 | ->where('price', Operator::GREATER_THAN, 100, false); 894 | foreach ($data as $key => $item) { 895 | echo $item->get("price"); // returns an integer 896 | } 897 | 898 | ``` 899 | 900 | 901 | If you want to loop through `$data` and obtain the current `$item` variable as an array you should set `false` as a second parameter in the static `make()` method: 902 | 903 | ```php 904 | $table = Block::make($dataTable, false); 905 | $data = $table->select('product', 'price')->where('price', Operator::GREATER_THAN, 100, false); 906 | foreach ($data as $key => $item) { 907 | print_r($item); // $item is an array 908 | } 909 | ``` 910 | 911 | ### The `iterateBlock()` method 912 | With the `iterateBlock()` method, you can switch from array or Block for nested lists inside the main Block object if you already instanced it as a Block object. 913 | In the example above, you have the `$table` Block object. 914 | You can loop across the items of the `$table` object. 915 | If each item in the loop is itself an array (so an array of arrays), you can retrieve it as an array or a Block, depending on your needs: 916 | 917 | ```php 918 | $table = Block::make($dataTable); 919 | foreach ($table as $key => $item) { 920 | expect($item)->toBeInstanceOf(Block::class); 921 | expect($key)->toBeInt(); 922 | expect($item->get("price"))->toBeGreaterThan(10); 923 | } 924 | 925 | // iterateBlock(false if you need array instad of a nested Block) 926 | foreach ($table->iterateBlock(false) as $key => $item) { 927 | expect($item)->toBeArray(); 928 | expect($key)->toBeInt(); 929 | expect($item["price"])->toBeGreaterThan(10); 930 | } 931 | ``` 932 | 933 | ### Using forEach() method 934 | The `Block` class implements the `forEach()`method. 935 | > If you need to walk through the `Block` object, you can use the `forEach()` method. 936 | You can specify the function as an argument of the `forEach()` method to manage each single element. 937 | 938 | ```php 939 | $url = "https://dummyjson.com/posts"; 940 | $posts = Block::fromJsonUrl($url) // Load the Block from the remote URL 941 | ->getBlock("posts") // get the `posts` as Block object 942 | ->where( 943 | field:"tags", 944 | operator: Operator::HAS, 945 | value: "love", 946 | preseveKeys: false, 947 | ) // filter the posts, selecting only the posts with tags "love" 948 | ->forEach(fn($element): array => [ 949 | "title" => strtoupper((string) $element->get("title")), 950 | "tags" => count($element->get("tags")), 951 | ]); 952 | // The `$posts` object is an instance of the `Block` class. 953 | // The `$posts` object contains the items that matches the `where` method. 954 | // You can access to the elements via the nested keys 955 | // $posts->get("0.title"); // "HOPES AND DREAMS WERE DASHED THAT DAY." 956 | // $posts->get("0.tags"); // 3 957 | ``` 958 | 959 | ## Validating Data 960 | 961 | You can validate the data in the Block object with JSON schema. 962 | JSON Schema is a vocabulary used to annotate and validate JSON documents. 963 | 964 | > More info about JSON Schema: https://json-schema.org/learn/getting-started-step-by-step 965 | 966 | If you need some common/popular schemas, you can find some schemas here: https://www.schemastore.org/json/ 967 | For example: 968 | 969 | - Schema for validating the `composer.json` file: https://getcomposer.org/schema.json 970 | - Schema for validating the GitHub Actions workflows: https://json.schemastore.org/github-workflow.json 971 | 972 | Or you can build your own schema according to the JSON schema specifications: https://json-schema.org/learn/getting-started-step-by-step#create-a-schema-definition 973 | 974 | 975 | ```php 976 | $file = "./.github/workflows/run-tests.yml"; 977 | $workflow = Block::fromYamlFile($file); 978 | $workflow->validateJsonViaUrl( 979 | 'https://json.schemastore.org/github-workflow' 980 | ); // TRUE if the Block is a valid GitHub Actions Workflow 981 | ``` 982 | 983 | Or you can define your own schema: 984 | 985 | ```php 986 | $schemaJson = <<<'JSON' 987 | { 988 | "type": "array", 989 | "items" : { 990 | "type": "object", 991 | "properties": { 992 | "name": { 993 | "type": "string" 994 | }, 995 | "fruit": { 996 | "type": "string" 997 | }, 998 | "wikipedia": { 999 | "type": "string" 1000 | }, 1001 | "color": { 1002 | "type": "string" 1003 | }, 1004 | "rating": { 1005 | "type": "number" 1006 | } 1007 | } 1008 | } 1009 | } 1010 | JSON; 1011 | ``` 1012 | And then validate it with your Block object: 1013 | 1014 | ```php 1015 | $fruitsArray = [ 1016 | [ 1017 | 'name' => 'Avocado', 1018 | 'fruit' => '🥑', 1019 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Avocado', 1020 | 'color' => 'green', 1021 | 'rating' => 8, 1022 | ], 1023 | [ 1024 | 'name' => 'Apple', 1025 | 'fruit' => '🍎', 1026 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Apple', 1027 | 'color' => 'red', 1028 | 'rating' => 7, 1029 | ], 1030 | [ 1031 | 'name' => 'Banana', 1032 | 'fruit' => '🍌', 1033 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Banana', 1034 | 'color' => 'yellow', 1035 | 'rating' => 8.5, 1036 | ], 1037 | [ 1038 | 'name' => 'Cherry', 1039 | 'fruit' => '🍒', 1040 | 'wikipedia' => 'https://en.wikipedia.org/wiki/Cherry', 1041 | 'color' => 'red', 1042 | 'rating' => 9, 1043 | ], 1044 | ]; 1045 | 1046 | $data = Block::make($fruitsArray); 1047 | $data->validateJsonWithSchema($schemaJson); 1048 | // true if the Block is valid. 1049 | ``` 1050 | 1051 | If you are starting to use the Data Block and testing it just to gain confidence, implementing different scenarios, or testing a non-valid JSON, try changing the "rating" type from number to integer (the validation should fail because in the JSON, we have ratings with decimals). 1052 | And, yes, to change on the fly the schema you can use the Block object :) 1053 | 1054 | ```php 1055 | // load the schema as Block object... 1056 | $schemaBlock = Block::fromJsonString($schemaJson); 1057 | // so that you can change the type 1058 | $schemaBlock->set( 1059 | "items.properties.rating.type", 1060 | "integer" 1061 | ); 1062 | // the validation should be false because integer vs number 1063 | $data->validateJsonWithSchema( 1064 | $schemaBlock->toJson() 1065 | ); 1066 | ``` 1067 | 1068 | ## Applying functions 1069 | 1070 | The `applyField()` method applies a callable function to the value of a specified field and sets the result to another field. This method supports method chaining. 1071 | 1072 | ### Parameters 1073 | 1074 | - `key` (string|int): The key of the field whose value will be processed. 1075 | - `targetKey` (string|int): The key where the result of the callable function should be stored. 1076 | - `callable` (callable): The function to apply to the field value. This function should accept a single argument (the value of the field) and return the processed value. 1077 | 1078 | ### Returns 1079 | 1080 | - `self`: Returns the instance of the class for method chaining. 1081 | 1082 | ### Example Usage 1083 | 1084 | ```php 1085 | set('name', 'John Doe') 1090 | ->applyField('name', 'uppercase_name', function($value) { 1091 | return strtoupper($value); 1092 | }); 1093 | 1094 | echo $object->get('uppercase_name'); // Outputs: JOHN DOE 1095 | 1096 | ## Testing 1097 | 1098 | ```bash 1099 | composer test 1100 | ``` 1101 | 1102 | ## Changelog 1103 | 1104 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 1105 | 1106 | ## Contributing 1107 | 1108 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 1109 | 1110 | ## Security Vulnerabilities 1111 | 1112 | Please review [our security policy](../../security/policy) on reporting security vulnerabilities. 1113 | 1114 | ## License 1115 | 1116 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 1117 | 1118 | ## Thanks to 1119 | 1120 | Thank you to everyone who has provided feedback, opened issues, or created pull requests. A special thanks to all the contributors! You can view the full list of contributors [in this section](https://github.com/Hi-Folks/data-block/graphs/contributors). 1121 | 1122 | The PHP ecosystem offers many tools that help developers enhance productivity, reliability, and efficiency. One such tool is JetBrains PhpStorm. JetBrains supports the open-source community by offering licenses for various open-source projects. More information can be found in the [Open Source section of the JetBrains website](https://jb.gg/OpenSourceSupport). 1123 | 1124 | PhpStorm logo 1125 | 1126 | 1127 | I’m thrilled to share that JetBrains has provided an Open Source license for the PHP Data Block project. 1128 | This recognition of PHP Data Block as a valuable open-source software fills me with joy. 1129 | 1130 | Thank you! 1131 | 1132 | [Roberto](https://github.com/roberto-butti) 1133 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hi-folks/data-block", 3 | "description": "Data class for managing nested arrays and JSON data.", 4 | "keywords": [ 5 | "Hi-Folks", 6 | "JSON", 7 | "data", 8 | "PHP", 9 | "array" 10 | ], 11 | "homepage": "https://github.com/hi-folks/data-blocks", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Roberto B.", 16 | "email": "roberto.butti@gmail.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.2|^8.3|^8.4", 22 | "swaggest/json-schema": "^0.12.42", 23 | "symfony/yaml": "^6.4|^7.1" 24 | }, 25 | "require-dev": { 26 | "laravel/pint": "^1.2", 27 | "pestphp/pest": "^3.0", 28 | "phpstan/phpstan": "^2", 29 | "rector/rector": "^2" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "HiFolks\\DataType\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "HiFolks\\DataBlocks\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "all": [ 43 | "@test", "@format", "@phpstan", "@rector" 44 | ], 45 | "complete": [ 46 | "@test-with-url", "@format", "@phpstan" 47 | ], 48 | "test": "vendor/bin/pest --exclude-group=url", 49 | "test-with-url": "vendor/bin/pest", 50 | "test-coverage": "vendor/bin/pest --coverage", 51 | "phpstan": "vendor/bin/phpstan", 52 | "format": "vendor/bin/pint", 53 | "rector": "vendor/bin/rector" 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "allow-plugins": { 58 | "pestphp/pest-plugin": true 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /cover-data-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hi-Folks/data-block/e1800564d21821eb23ee6b5c3cad01044d47df03/cover-data-block.png -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./app 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "per" 3 | } 4 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/src', 10 | __DIR__ . '/tests', 11 | ]) 12 | // uncomment to reach your current PHP version 13 | ->withPhpSets( 14 | php82: true, 15 | ) 16 | ->withPreparedSets( 17 | deadCode: true, 18 | codeQuality: true, 19 | earlyReturn: true, 20 | typeDeclarations: true, 21 | privatization: true, 22 | // naming: true 23 | ); 24 | -------------------------------------------------------------------------------- /src/Block.php: -------------------------------------------------------------------------------- 1 | 24 | * @implements ArrayAccess 25 | */ 26 | final class Block implements Iterator, ArrayAccess, Countable 27 | { 28 | use QueryableBlock; 29 | use EditableBlock; 30 | use ExportableBlock; 31 | use LoadableBlock; 32 | use IteratableBlock; 33 | use ValidableBlock; 34 | use FormattableBlock; 35 | use TypeableBlock; 36 | 37 | 38 | /** @var array */ 39 | private array $data; 40 | 41 | /** @param array $data */ 42 | public function __construct(array $data = [], private bool $iteratorReturnsBlock = true) 43 | { 44 | $this->data = $data; 45 | } 46 | 47 | public function iterateBlock(bool $returnsBlock = true): self 48 | { 49 | $this->iteratorReturnsBlock = $returnsBlock; 50 | return $this; 51 | } 52 | 53 | 54 | 55 | 56 | /** @param array $data */ 57 | public static function make(array $data = [], bool $iteratorReturnsBlock = true): self 58 | { 59 | return new self($data, $iteratorReturnsBlock); 60 | } 61 | 62 | 63 | 64 | public function count(): int 65 | { 66 | return count($this->data); 67 | } 68 | 69 | 70 | /** @param array $encodedJson */ 71 | public static function fromEncodedJson(mixed $encodedJson): self 72 | { 73 | return new self($encodedJson); 74 | } 75 | /** 76 | * Get the element with $key 77 | * 78 | * @param non-empty-string $charNestedKey 79 | */ 80 | public function get(int|string $key, mixed $defaultValue = null, string $charNestedKey = "."): mixed 81 | { 82 | if (is_string($key)) { 83 | $keyString = strval($key); 84 | if (str_contains($keyString, $charNestedKey)) { 85 | $nestedValue = $this->data; 86 | foreach (explode($charNestedKey, $keyString) as $nestedKey) { 87 | if (is_array($nestedValue) && array_key_exists($nestedKey, $nestedValue)) { 88 | $nestedValue = $nestedValue[$nestedKey]; 89 | } elseif ($nestedValue instanceof Block) { 90 | $nestedValue = $nestedValue->get($nestedKey); 91 | } else { 92 | return $defaultValue; 93 | } 94 | } 95 | return $nestedValue; 96 | } 97 | } 98 | return $this->data[$key] ?? $defaultValue; 99 | } 100 | 101 | 102 | 103 | 104 | /** 105 | * Get the element with $key as Block object 106 | * This is helpful when the element is an array, and you 107 | * need to get the Block object instead of the classic array 108 | * In the case the $key doesn't exist, an empty Block can be returned 109 | * @param non-empty-string $charNestedKey 110 | */ 111 | public function getBlock(mixed $key, mixed $defaultValue = null, string $charNestedKey = "."): self 112 | { 113 | $value = $this->getBlockNullable($key, $defaultValue, $charNestedKey); 114 | if (is_null($value)) { 115 | return Block::make([]); 116 | } 117 | return $value; 118 | } 119 | 120 | /** 121 | * Get the element with $key as Block object 122 | * This is helpful when the element is an array, and you 123 | * need to get the Block object instead of the classic array 124 | * In the case the $key doesn't exist, null can be returned 125 | * @param non-empty-string $charNestedKey 126 | */ 127 | public function getBlockNullable(mixed $key, mixed $defaultValue = null, string $charNestedKey = "."): ?self 128 | { 129 | $value = $this->get($key, $defaultValue, $charNestedKey); 130 | if (is_null($value)) { 131 | return null; 132 | } 133 | if (is_scalar($value)) { 134 | return self::make([$value]); 135 | } 136 | if (is_array($value)) { 137 | return self::make($value); 138 | } 139 | if ($value instanceof Block) { 140 | return $value; 141 | } 142 | return Block::make([]); 143 | } 144 | 145 | 146 | /** 147 | * Set a value to a specific $key 148 | * You can use the dot notation for setting a nested value. 149 | * @param non-empty-string $charNestedKey 150 | */ 151 | public function set(int|string $key, mixed $value, string $charNestedKey = "."): self 152 | { 153 | if (is_string($key)) { 154 | $array = &$this->data; 155 | $keys = explode($charNestedKey, $key); 156 | foreach ($keys as $i => $key) { 157 | if (count($keys) === 1) { 158 | break; 159 | } 160 | unset($keys[$i]); 161 | 162 | if (!isset($array[$key]) || !is_array($array[$key])) { 163 | $array[$key] = []; 164 | } 165 | 166 | $array = &$array[$key]; 167 | } 168 | 169 | $array[array_shift($keys)] = $value; 170 | return $this; 171 | } 172 | $this->data[$key] = $value; 173 | return $this; 174 | } 175 | 176 | 177 | 178 | /** 179 | * @return Block object that contains the key/value pairs for each index in the array 180 | */ 181 | public function entries(): self 182 | { 183 | $pairs = []; 184 | foreach ($this->data as $k => $v) { 185 | $pairs[] = [$k, $v]; 186 | } 187 | 188 | return self::make($pairs); 189 | } 190 | 191 | /** 192 | * @return Block object that contains the key/value pairs for each index in the array 193 | */ 194 | public function values(): self 195 | { 196 | $pairs = $this->data; 197 | return self::make($pairs); 198 | } 199 | 200 | /** 201 | * Returns a new array [] or a new Blcok object that contains the keys 202 | * for each index in the Block object 203 | * It returns Block or [] depending on $returnArrClass value 204 | * 205 | * @param bool $returnBlockClass true if you need Block object 206 | * @return array|Block 207 | */ 208 | public function keys(bool $returnBlockClass = false): array|Block 209 | { 210 | if ($returnBlockClass) { 211 | return self::make(array_keys($this->data)); 212 | } 213 | 214 | return array_keys($this->data); 215 | } 216 | 217 | public function has(mixed $value): bool 218 | { 219 | return in_array($value, $this->values()->toArray()); 220 | } 221 | 222 | public function hasKey(string|int $key): bool 223 | { 224 | /** @var array $keys */ 225 | $keys = $this->keys(); 226 | return in_array($key, $keys); 227 | } 228 | 229 | /** 230 | * Applies a callable function to a field and sets the result to a target field. 231 | * 232 | * @param string|int $key The key of the field to be processed. 233 | * @param string|int $targetKey The key where the result should be stored. 234 | * @param callable $callable The function to apply to the field value. 235 | * 236 | * @return self Returns the instance of the class for method chaining. 237 | */ 238 | public function applyField( 239 | string|int $key, 240 | string|int $targetKey, 241 | callable $callable, 242 | ): self { 243 | $this->set($targetKey, $callable($this->get($key))); 244 | return $this; 245 | } 246 | 247 | 248 | 249 | 250 | 251 | } 252 | -------------------------------------------------------------------------------- /src/Enums/Operator.php: -------------------------------------------------------------------------------- 1 | '; 11 | public const LESS_THAN = '<'; 12 | public const GREATER_THAN_OR_EQUAL = '>='; 13 | public const LESS_THAN_OR_EQUAL = '<='; 14 | public const NOT_EQUAL = '!='; 15 | public const STRICT_NOT_EQUAL = '!=='; 16 | public const IN = 'in'; 17 | public const HAS = 'has'; 18 | public const LIKE = 'like'; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Traits/EditableBlock.php: -------------------------------------------------------------------------------- 1 | |Block $data 13 | * @return $this 14 | */ 15 | public function append(array|Block $data, ?string $key = null): self 16 | { 17 | if ($data instanceof Block) { 18 | $this->data = array_merge($this->data, $data->toArray()); 19 | } else { 20 | $this->data = array_merge($this->data, $data); 21 | } 22 | 23 | return $this; 24 | } 25 | 26 | public function appendItem(mixed $data, ?string $key = null): self 27 | { 28 | if (is_null($key)) { 29 | $this->data[] = $data; 30 | } else { 31 | $this->data[$key] = $data; 32 | } 33 | 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * @param array|Block $value 40 | */ 41 | private static function forceBlock(array|Block $value): Block 42 | { 43 | if (!$value instanceof Block) { 44 | return Block::make($value); 45 | } 46 | return $value; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Traits/ExportableBlock.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(): array 16 | { 17 | return $this->data; 18 | } 19 | 20 | 21 | /** 22 | * Returns the JSON String (pretty format by default) 23 | * @return string|false 24 | */ 25 | public function toJson(): string|false 26 | { 27 | return json_encode($this->data, JSON_PRETTY_PRINT); 28 | } 29 | 30 | public function dumpJson(): void 31 | { 32 | echo $this->toJson(); 33 | } 34 | public function dump(): void 35 | { 36 | var_dump($this); 37 | } 38 | 39 | public function toJsonObject(): mixed 40 | { 41 | $jsonString = json_encode($this->data); 42 | if ($jsonString === false) { 43 | return false; 44 | } 45 | return json_decode($jsonString, associative: false); 46 | } 47 | 48 | 49 | /** 50 | * Returns the YAML String 51 | */ 52 | public function toYaml(): string 53 | { 54 | return Yaml::dump($this->data, 3, 2); 55 | } 56 | 57 | /** 58 | * Saves the JSON String to a file 59 | * @param string $filename file name for example "./file.json" 60 | * @param bool $overwrite if the file already exists you can force overwriting 61 | * @return bool true if the file is saved, if already exists and you don't want to 62 | * force overwiting the file it returns false 63 | */ 64 | public function saveToJson(string $filename, bool $overwrite = false): bool 65 | { 66 | if (file_exists($filename) && !$overwrite) { 67 | return false; 68 | } 69 | 70 | file_put_contents($filename, $this->toJson()); 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Traits/FormattableBlock.php: -------------------------------------------------------------------------------- 1 | get($key, $defaultValue, $charNestedKey); 23 | if (is_null($value)) { 24 | return null; 25 | } 26 | $date = new \DateTimeImmutable($value); 27 | 28 | return $date->format($format); 29 | } 30 | 31 | /** 32 | * Get and format a byte value in KB, MB, GB, etc. 33 | * @param mixed $key the key, can be nested for example "some.filesize" 34 | * @param mixed $defaultValue the value returned if the key does not exist 35 | * @param non-empty-string $charNestedKey the separator for nested keys, default is "." 36 | * @param int $precision the number of decimal points for the formatted output, default is 2 37 | */ 38 | public function getFormattedByte( 39 | mixed $key, 40 | int $precision = 2, 41 | mixed $defaultValue = null, 42 | string $charNestedKey = ".", 43 | ): string { 44 | $bytes = $this->get($key, $defaultValue, $charNestedKey); 45 | if (is_null($bytes)) { 46 | return '0 B'; 47 | } 48 | 49 | $kilobyte = 1024; 50 | $megabyte = $kilobyte * 1024; 51 | $gigabyte = $megabyte * 1024; 52 | $terabyte = $gigabyte * 1024; 53 | 54 | if ($bytes < $kilobyte) { 55 | return $bytes . ' B'; 56 | } 57 | 58 | if ($bytes < $megabyte) { 59 | return number_format($bytes / $kilobyte, $precision) . ' KB'; 60 | } 61 | 62 | if ($bytes < $gigabyte) { 63 | return number_format($bytes / $megabyte, $precision) . ' MB'; 64 | } 65 | 66 | if ($bytes < $terabyte) { 67 | return number_format($bytes / $gigabyte, $precision) . ' GB'; 68 | } 69 | 70 | return number_format($bytes / $terabyte, $precision) . ' TB'; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Traits/IteratableBlock.php: -------------------------------------------------------------------------------- 1 | iteratorReturnsBlock) { 12 | $current = current($this->data); 13 | if (is_array($current)) { 14 | return self::make($current); 15 | } 16 | return $current; 17 | } 18 | return current($this->data); 19 | } 20 | 21 | public function next(): void 22 | { 23 | next($this->data); 24 | } 25 | 26 | /** 27 | * Return the key of the current element 28 | * 29 | * @link https://php.net/manual/en/iterator.key.php 30 | * 31 | * @return string|int|null scalar on success, or null on failure. 32 | */ 33 | public function key(): string|int|null 34 | { 35 | return key($this->data); 36 | } 37 | 38 | public function valid(): bool 39 | { 40 | return !is_null($this->key()); 41 | } 42 | 43 | public function rewind(): void 44 | { 45 | reset($this->data); 46 | } 47 | 48 | public function offsetExists(mixed $offset): bool 49 | { 50 | return array_key_exists($offset, $this->data); 51 | } 52 | 53 | public function offsetGet(mixed $offset): mixed 54 | { 55 | return $this->get($offset); 56 | } 57 | 58 | public function offsetSet(mixed $offset, mixed $value): void 59 | { 60 | if (is_null($offset)) { 61 | $this->data[] = $value; 62 | } else { 63 | $this->data[$offset] = $value; 64 | } 65 | } 66 | 67 | public function offsetUnset(mixed $offset): void 68 | { 69 | unset($this->data[$offset]); 70 | } 71 | 72 | /** 73 | * It executes a provided function ($callback) once for each element. 74 | * @param callable $callback the function to call for each element 75 | */ 76 | public function forEach(callable $callback): self 77 | { 78 | $result = []; 79 | foreach ($this as $key => $item) { 80 | $result[$key] = $callback($item); 81 | } 82 | return self::make($result); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Traits/LoadableBlock.php: -------------------------------------------------------------------------------- 1 | $json */ 14 | $json = json_decode($jsonString, associative: true); 15 | return self::make($json); 16 | } 17 | 18 | public static function fromJsonFile(string $jsonFile): self 19 | { 20 | if (file_exists($jsonFile)) { 21 | $content = file_get_contents($jsonFile); 22 | if ($content === false) { 23 | return self::make([]); 24 | } 25 | return self::fromJsonString($content); 26 | } 27 | return self::make([]); 28 | } 29 | 30 | /** 31 | * Load Block object from a remote JSON. 32 | * @param $jsonUrl the URL for loading the JSON, for example https://dummyjson.com/posts 33 | * @param null|array $headers the optional headers, set [] if you want to avoid headers 34 | */ 35 | public static function fromJsonUrl(string $jsonUrl, ?array $headers = null): self 36 | { 37 | if (is_null($headers)) { 38 | $headers = [ 39 | 'Accept-language: en', 40 | 'User-Agent: hi-folks/data-block', 41 | ]; 42 | } 43 | $options = [ 44 | 'http' => [ 45 | 'method' => "GET", 46 | 'header' => $headers, 47 | ], 48 | ]; 49 | $context = stream_context_create($options); 50 | $content = file_get_contents($jsonUrl, context: $context); 51 | //var_dump($jsonUrl, $content); 52 | if ($content === false) { 53 | return self::make([]); 54 | } 55 | return self::fromJsonString($content); 56 | 57 | } 58 | 59 | public static function fromYamlFile(string $yamlFile): self 60 | { 61 | if (file_exists($yamlFile)) { 62 | $content = file_get_contents($yamlFile); 63 | if ($content === false) { 64 | return self::make([]); 65 | } 66 | return self::fromYamlString($content); 67 | } 68 | return self::make([]); 69 | } 70 | 71 | public static function fromYamlString(string $yamlString = ""): self 72 | { 73 | /** @var array $yaml */ 74 | $yaml = Yaml::parse($yamlString); 75 | return self::make($yaml); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Traits/QueryableBlock.php: -------------------------------------------------------------------------------- 1 | count() > 0; 23 | } 24 | 25 | public function where( 26 | string|int $field, 27 | mixed $operator = Operator::EQUAL, 28 | mixed $value = null, 29 | bool $preseveKeys = true, 30 | ): self { 31 | 32 | if (func_num_args() === 1) { 33 | $value = true; 34 | $operator = Operator::EQUAL; 35 | } 36 | if (func_num_args() === 2) { 37 | $value = $operator; 38 | $operator = Operator::EQUAL; 39 | } 40 | 41 | $returnData = []; 42 | 43 | foreach ($this as $key => $element) { 44 | $elementToCheck = $element; 45 | if (is_array($element)) { 46 | $elementToCheck = Block::make($element, $this->iteratorReturnsBlock); 47 | } 48 | if (! $elementToCheck instanceof Block) { 49 | return Block::make([], $this->iteratorReturnsBlock); 50 | } 51 | 52 | $found = match ($operator) { 53 | Operator::EQUAL => ($elementToCheck->get($field) == $value), 54 | Operator::GREATER_THAN => $elementToCheck->get($field) > $value, 55 | Operator::LESS_THAN => $elementToCheck->get($field) < $value, 56 | Operator::GREATER_THAN_OR_EQUAL => $elementToCheck->get($field) >= $value, 57 | Operator::LESS_THAN_OR_EQUAL => $elementToCheck->get($field) <= $value, 58 | Operator::NOT_EQUAL => $elementToCheck->get($field) != $value, 59 | Operator::STRICT_NOT_EQUAL => $elementToCheck->get($field) !== $value, 60 | Operator::IN => in_array($elementToCheck->get($field), $value), 61 | Operator::HAS => in_array($value, $elementToCheck->get($field)), 62 | Operator::LIKE => str_contains($elementToCheck->get($field), (string) $value), 63 | default => $elementToCheck->get($field) === $value, 64 | }; 65 | if ($found) { 66 | 67 | if ($preseveKeys) { 68 | $returnData[$key] = $element instanceof Block ? $element->toArray() : $element; 69 | } else { 70 | $returnData[] = $element instanceof Block ? $element->toArray() : $element; 71 | ; 72 | } 73 | 74 | } 75 | } 76 | return self::make($returnData, $this->iteratorReturnsBlock); 77 | } 78 | 79 | 80 | public function orderBy(string|int $field, string $order = 'asc'): self 81 | { 82 | $map = []; 83 | $array = $this->data; 84 | 85 | foreach ($this as $key => $item) { 86 | $map[$key] = $item->get($field); 87 | } 88 | if ($order === 'desc') { 89 | array_multisort( 90 | $map, 91 | SORT_DESC, 92 | $array, 93 | ); 94 | 95 | } else { 96 | array_multisort($map, $array) ; 97 | } 98 | 99 | return self::make($array, $this->iteratorReturnsBlock); 100 | } 101 | 102 | public function select(int|string ...$columns): self 103 | { 104 | $table = self::make([], $this->iteratorReturnsBlock); 105 | 106 | foreach ($this->data as $row) { 107 | if (is_array($row)) { 108 | /** @var Block $row */ 109 | $row = self::make($row, $this->iteratorReturnsBlock); 110 | } 111 | $newRow = []; 112 | foreach ($columns as $column) { 113 | /** @var Block $row */ 114 | $value = $row->get($column); 115 | $newRow[$column] = $value; 116 | } 117 | 118 | $table->appendItem($newRow); 119 | } 120 | 121 | return $table; 122 | } 123 | 124 | /** 125 | * Groups the elements of the Block by a specified field. 126 | * 127 | * This method takes a field name as an argument and groups the elements of the 128 | * Block object based on the values of that field. Each element is grouped into 129 | * an associative array where the keys are the values of the specified field 130 | * and the values are arrays of elements that share that key. 131 | * 132 | * @param string|int $field The field name to group by. 133 | * @return self A new Block instance with the grouped elements. 134 | * 135 | */ 136 | public function groupBy(string|int $field): self 137 | { 138 | $result = []; 139 | 140 | foreach ($this as $value) { 141 | $property = $value->get($field); 142 | $property = self::castVariableForStrval($property); 143 | if (!$property) { 144 | continue; 145 | } 146 | if (! array_key_exists(strval($property), $result)) { 147 | $result[$property] = []; 148 | } 149 | $result[$property][] = $value->toArray(); 150 | } 151 | 152 | return self::make($result); 153 | } 154 | 155 | public function groupByFunction(callable $groupFunction): self 156 | { 157 | $result = []; 158 | 159 | foreach ($this->data as $item) { 160 | // Call the closure to determine the group key 161 | $groupKey = $groupFunction($item); 162 | 163 | // Group items under the same key 164 | if (!array_key_exists($groupKey, $result)) { 165 | $result[$groupKey] = []; 166 | } 167 | 168 | $result[$groupKey][] = $item; 169 | } 170 | return self::make($result); 171 | } 172 | 173 | 174 | private static function castVariableForStrval(mixed $property): bool|float|int|string|null 175 | { 176 | return match (gettype($property)) { 177 | 'boolean' => $property, 178 | 'double' => $property, 179 | 'integer' => $property, 180 | 'string' => $property, 181 | default => null, 182 | }; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/Traits/TypeableBlock.php: -------------------------------------------------------------------------------- 1 | get($key, $defaultValue, $charNestedKey); 21 | if ($returnValue === null) { 22 | return $defaultValue; 23 | } 24 | 25 | if (is_scalar($returnValue)) { 26 | return strval($returnValue); 27 | } 28 | 29 | return $defaultValue; 30 | 31 | } 32 | 33 | /** 34 | * Return a forced string value from the get() method 35 | * @param int|string $key the field key , can be nested for example "commits.0.name" 36 | * @param string $defaultValue the default value returned if no value is found, by default is "" 37 | * @param non-empty-string $charNestedKey for nested field the . character is the default 38 | */ 39 | public function getStringStrict( 40 | int|string $key, 41 | string $defaultValue = "", 42 | string $charNestedKey = ".", 43 | ): string { 44 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 45 | if ($returnValue === null) { 46 | return $defaultValue; 47 | } 48 | 49 | if (is_scalar($returnValue)) { 50 | return strval($returnValue); 51 | } 52 | 53 | return $defaultValue; 54 | 55 | } 56 | 57 | /** 58 | * Return a forced integer value from the get() method 59 | * @param int|string $key the field key, can be nested for example "0.author.id" 60 | * @param int|null $defaultValue the default integer value returned if no value is found 61 | * @param non-empty-string $charNestedKey for nested field the . character is the default 62 | */ 63 | public function getInt(int|string $key, ?int $defaultValue = null, string $charNestedKey = "."): ?int 64 | { 65 | $returnValue = $this->get($key, null, $charNestedKey); 66 | 67 | if (is_scalar($returnValue)) { 68 | return intval($returnValue); 69 | } 70 | 71 | return $defaultValue; 72 | } 73 | 74 | /** 75 | * Return a forced integer value from the get() method 76 | * @param int|string $key the field key, can be nested for example "0.author.id" 77 | * @param int $defaultValue the default integer value returned if no value is found 78 | * @param non-empty-string $charNestedKey for nested field the . character is the default 79 | */ 80 | public function getIntStrict(int|string $key, int $defaultValue = 0, string $charNestedKey = "."): int 81 | { 82 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 83 | 84 | if ($returnValue === null) { 85 | return $defaultValue; 86 | } 87 | 88 | if (is_scalar($returnValue)) { 89 | return intval($returnValue); 90 | } 91 | 92 | return $defaultValue; 93 | } 94 | /** 95 | * Return a forced boolean value from the get() method 96 | * @param int|string $key the filed key , can be nested for example "commits.0.editable" 97 | * @param bool|null $defaultValue the default value returned if no value is found 98 | * @param non-empty-string $charNestedKey for nested field the . character is the default 99 | */ 100 | public function getBoolean( 101 | int|string $key, 102 | ?bool $defaultValue = null, 103 | string $charNestedKey = ".", 104 | ): ?bool { 105 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 106 | 107 | if (is_scalar($returnValue)) { 108 | return boolval($returnValue); 109 | } 110 | 111 | return $defaultValue; 112 | } 113 | 114 | /** 115 | * Return a forced boolean value from the get() method 116 | * @param int|string $key the filed key , can be nested for example "commits.0.editable" 117 | * @param bool $defaultValue the default value returned if no value is found 118 | * @param non-empty-string $charNestedKey for nested field the . character is the default 119 | */ 120 | public function getBooleanStrict( 121 | int|string $key, 122 | bool $defaultValue = false, 123 | string $charNestedKey = ".", 124 | ): ?bool { 125 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 126 | 127 | if (is_scalar($returnValue)) { 128 | return boolval($returnValue); 129 | } 130 | 131 | return $defaultValue; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Traits/ValidableBlock.php: -------------------------------------------------------------------------------- 1 | in($this->toJsonObject()); 17 | } catch (\Exception) { 18 | return false; 19 | } 20 | return true; 21 | 22 | } 23 | 24 | public function validateJsonSchemaGithubWorkflow(): bool 25 | { 26 | return $this->validateJsonViaUrl('https://json.schemastore.org/github-workflow'); 27 | } 28 | 29 | public function validateJsonWithSchema(string $schemaJson): bool 30 | { 31 | try { 32 | $schema 33 | = Schema::import(json_decode($schemaJson)); 34 | $schema->in($this->toJsonObject()); 35 | } catch (\Exception) { 36 | //echo $e->getMessage(); 37 | return false; 38 | } 39 | return true; 40 | } 41 | } 42 | --------------------------------------------------------------------------------