├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── honeystone-dto-tools.php ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── src ├── Attributes │ ├── Patch.php │ ├── ToMany.php │ └── ToOne.php ├── Casters │ ├── Contracts │ │ └── CastsValues.php │ ├── DateTimeCaster.php │ ├── EnumCaster.php │ ├── NullCaster.php │ └── ScalarCaster.php ├── Concerns │ ├── CreatableFromArray.php │ ├── CreatableFromSnake.php │ ├── HasStorableData.php │ ├── HasStorableRelationships.php │ ├── HasTransferableData.php │ ├── SerializesToArray.php │ └── SerializesToSnake.php ├── Contracts │ ├── Storable.php │ ├── StorableRelationships.php │ └── Transferable.php ├── Exceptions │ ├── RelationNotFoundException.php │ ├── RelationValidationException.php │ └── RequiredRelationNotLoadedException.php ├── Providers │ └── DtoToolsServiceProvider.php ├── Relationships.php └── Transformers │ ├── Concerns │ ├── ManipulatesData.php │ └── TransformsRelations.php │ ├── Contracts │ ├── MakesTransformers.php │ ├── ManipulatesSchema.php │ ├── TransformsCollections.php │ └── TransformsModels.php │ ├── ModelTransformer.php │ └── TransformerFactory.php ├── testbench.yaml ├── tests ├── Integration │ └── Transformers │ │ └── Concerns │ │ ├── Fixtures │ │ ├── Data │ │ │ ├── BarData.php │ │ │ ├── BazData.php │ │ │ └── FooData.php │ │ ├── Enums │ │ │ └── State.php │ │ ├── Models │ │ │ ├── Bar.php │ │ │ ├── Baz.php │ │ │ └── Foo.php │ │ └── Transformers │ │ │ ├── BarTransformer.php │ │ │ ├── BazTransformer.php │ │ │ ├── MapMethodTransformer.php │ │ │ ├── NoMapTransformer.php │ │ │ ├── OnlyPropertyTransformer.php │ │ │ └── RelationsTransformer.php │ │ └── ModelTransformerTest.php ├── Pest.php ├── TestCase.php └── Unit │ ├── Concerns │ ├── CreatableFromArray.php │ ├── CreatableFromSnakeTest.php │ ├── Fixtures │ │ ├── CastingData.php │ │ ├── CreationData.php │ │ ├── KeyTransformationData.php │ │ ├── PatchData.php │ │ ├── SnakeTransformationData.php │ │ └── TransformationData.php │ ├── HasStorableDataTest.php │ ├── SerialisesToArrayTest.php │ └── SerialisesToSnakeTest.php │ ├── Fixtures │ ├── FooData.php │ └── MetaData.php │ └── RelationshipsTest.php └── workbench ├── app ├── Models │ └── .gitkeep └── Providers │ └── WorkbenchServiceProvider.php ├── bootstrap ├── .gitkeep └── app.php ├── database ├── factories │ └── .gitkeep ├── migrations │ └── .gitkeep └── seeders │ ├── .gitkeep │ └── DatabaseSeeder.php ├── resources └── views │ └── .gitkeep └── routes ├── .gitkeep ├── console.php └── web.php /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = 120 8 | tab_width = 4 9 | trim_trailing_whitespace = true 10 | ij_continuation_indent_size = 8 11 | ij_formatter_off_tag = @formatter:off 12 | ij_formatter_on_tag = @formatter:on 13 | ij_formatter_tags_enabled = false 14 | ij_smart_tabs = false 15 | ij_visual_guides = 80,120 16 | ij_wrap_on_typing = false 17 | 18 | [*.blade.php] 19 | ij_visual_guides = none 20 | ij_blade_keep_indents_on_empty_lines = false 21 | 22 | [*.css] 23 | ij_visual_guides = none 24 | ij_css_align_closing_brace_with_properties = false 25 | ij_css_blank_lines_around_nested_selector = 1 26 | ij_css_blank_lines_between_blocks = 1 27 | ij_css_brace_placement = end_of_line 28 | ij_css_enforce_quotes_on_format = false 29 | ij_css_hex_color_long_format = false 30 | ij_css_hex_color_lower_case = false 31 | ij_css_hex_color_short_format = false 32 | ij_css_hex_color_upper_case = false 33 | ij_css_keep_blank_lines_in_code = 2 34 | ij_css_keep_indents_on_empty_lines = false 35 | ij_css_keep_single_line_blocks = false 36 | ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow 37 | ij_css_space_after_colon = true 38 | ij_css_space_before_opening_brace = true 39 | ij_css_use_double_quotes = true 40 | ij_css_value_alignment = do_not_align 41 | 42 | [*.feature] 43 | indent_size = 2 44 | ij_visual_guides = none 45 | ij_gherkin_keep_indents_on_empty_lines = false 46 | 47 | [*.haml] 48 | indent_size = 2 49 | ij_visual_guides = none 50 | ij_haml_keep_indents_on_empty_lines = false 51 | 52 | [*.less] 53 | indent_size = 2 54 | ij_visual_guides = none 55 | ij_less_align_closing_brace_with_properties = false 56 | ij_less_blank_lines_around_nested_selector = 1 57 | ij_less_blank_lines_between_blocks = 1 58 | ij_less_brace_placement = 0 59 | ij_less_enforce_quotes_on_format = false 60 | ij_less_hex_color_long_format = false 61 | ij_less_hex_color_lower_case = false 62 | ij_less_hex_color_short_format = false 63 | ij_less_hex_color_upper_case = false 64 | ij_less_keep_blank_lines_in_code = 2 65 | ij_less_keep_indents_on_empty_lines = false 66 | ij_less_keep_single_line_blocks = false 67 | ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow 68 | ij_less_space_after_colon = true 69 | ij_less_space_before_opening_brace = true 70 | ij_less_use_double_quotes = true 71 | ij_less_value_alignment = 0 72 | 73 | [*.sass] 74 | indent_size = 2 75 | ij_visual_guides = none 76 | ij_sass_align_closing_brace_with_properties = false 77 | ij_sass_blank_lines_around_nested_selector = 1 78 | ij_sass_blank_lines_between_blocks = 1 79 | ij_sass_brace_placement = 0 80 | ij_sass_enforce_quotes_on_format = false 81 | ij_sass_hex_color_long_format = false 82 | ij_sass_hex_color_lower_case = false 83 | ij_sass_hex_color_short_format = false 84 | ij_sass_hex_color_upper_case = false 85 | ij_sass_keep_blank_lines_in_code = 2 86 | ij_sass_keep_indents_on_empty_lines = false 87 | ij_sass_keep_single_line_blocks = false 88 | ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow 89 | ij_sass_space_after_colon = true 90 | ij_sass_space_before_opening_brace = true 91 | ij_sass_use_double_quotes = true 92 | ij_sass_value_alignment = 0 93 | 94 | [*.scss] 95 | ij_visual_guides = none 96 | ij_scss_align_closing_brace_with_properties = false 97 | ij_scss_blank_lines_around_nested_selector = 1 98 | ij_scss_blank_lines_between_blocks = 1 99 | ij_scss_brace_placement = 0 100 | ij_scss_enforce_quotes_on_format = true 101 | ij_scss_hex_color_long_format = true 102 | ij_scss_hex_color_lower_case = true 103 | ij_scss_hex_color_short_format = false 104 | ij_scss_hex_color_upper_case = false 105 | ij_scss_keep_blank_lines_in_code = 2 106 | ij_scss_keep_indents_on_empty_lines = false 107 | ij_scss_keep_single_line_blocks = true 108 | ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow 109 | ij_scss_space_after_colon = true 110 | ij_scss_space_before_opening_brace = true 111 | ij_scss_use_double_quotes = false 112 | ij_scss_value_alignment = 0 113 | 114 | [*.twig] 115 | ij_visual_guides = none 116 | ij_twig_keep_indents_on_empty_lines = false 117 | ij_twig_spaces_inside_comments_delimiters = true 118 | ij_twig_spaces_inside_delimiters = true 119 | ij_twig_spaces_inside_variable_delimiters = true 120 | 121 | [*.vue] 122 | indent_size = 2 123 | tab_width = 2 124 | ij_continuation_indent_size = 4 125 | ij_vue_indent_children_of_top_level = template 126 | ij_vue_interpolation_new_line_after_start_delimiter = true 127 | ij_vue_interpolation_new_line_before_end_delimiter = true 128 | ij_vue_interpolation_wrap = off 129 | ij_vue_keep_indents_on_empty_lines = false 130 | ij_vue_spaces_within_interpolation_expressions = true 131 | 132 | [.editorconfig] 133 | ij_visual_guides = none 134 | ij_editorconfig_align_group_field_declarations = false 135 | ij_editorconfig_space_after_colon = false 136 | ij_editorconfig_space_after_comma = true 137 | ij_editorconfig_space_before_colon = false 138 | ij_editorconfig_space_before_comma = false 139 | ij_editorconfig_spaces_around_assignment_operators = true 140 | 141 | [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}] 142 | ij_visual_guides = none 143 | ij_xml_align_attributes = true 144 | ij_xml_align_text = false 145 | ij_xml_attribute_wrap = normal 146 | ij_xml_block_comment_at_first_column = true 147 | ij_xml_keep_blank_lines = 2 148 | ij_xml_keep_indents_on_empty_lines = false 149 | ij_xml_keep_line_breaks = true 150 | ij_xml_keep_line_breaks_in_text = true 151 | ij_xml_keep_whitespaces = false 152 | ij_xml_keep_whitespaces_around_cdata = preserve 153 | ij_xml_keep_whitespaces_inside_cdata = false 154 | ij_xml_line_comment_at_first_column = true 155 | ij_xml_space_after_tag_name = false 156 | ij_xml_space_around_equals_in_attribute = false 157 | ij_xml_space_inside_empty_tag = false 158 | ij_xml_text_wrap = normal 159 | 160 | [{*.ats,*.ts}] 161 | ij_continuation_indent_size = 4 162 | ij_typescript_align_imports = false 163 | ij_typescript_align_multiline_array_initializer_expression = false 164 | ij_typescript_align_multiline_binary_operation = false 165 | ij_typescript_align_multiline_chained_methods = false 166 | ij_typescript_align_multiline_extends_list = false 167 | ij_typescript_align_multiline_for = true 168 | ij_typescript_align_multiline_parameters = true 169 | ij_typescript_align_multiline_parameters_in_calls = false 170 | ij_typescript_align_multiline_ternary_operation = false 171 | ij_typescript_align_object_properties = 0 172 | ij_typescript_align_union_types = false 173 | ij_typescript_align_var_statements = 0 174 | ij_typescript_array_initializer_new_line_after_left_brace = false 175 | ij_typescript_array_initializer_right_brace_on_new_line = false 176 | ij_typescript_array_initializer_wrap = off 177 | ij_typescript_assignment_wrap = off 178 | ij_typescript_binary_operation_sign_on_next_line = false 179 | ij_typescript_binary_operation_wrap = off 180 | ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** 181 | ij_typescript_blank_lines_after_imports = 1 182 | ij_typescript_blank_lines_around_class = 1 183 | ij_typescript_blank_lines_around_field = 0 184 | ij_typescript_blank_lines_around_field_in_interface = 0 185 | ij_typescript_blank_lines_around_function = 1 186 | ij_typescript_blank_lines_around_method = 1 187 | ij_typescript_blank_lines_around_method_in_interface = 1 188 | ij_typescript_block_brace_style = end_of_line 189 | ij_typescript_call_parameters_new_line_after_left_paren = false 190 | ij_typescript_call_parameters_right_paren_on_new_line = false 191 | ij_typescript_call_parameters_wrap = off 192 | ij_typescript_catch_on_new_line = false 193 | ij_typescript_chained_call_dot_on_new_line = true 194 | ij_typescript_class_brace_style = end_of_line 195 | ij_typescript_comma_on_new_line = false 196 | ij_typescript_do_while_brace_force = never 197 | ij_typescript_else_on_new_line = false 198 | ij_typescript_enforce_trailing_comma = keep 199 | ij_typescript_extends_keyword_wrap = off 200 | ij_typescript_extends_list_wrap = off 201 | ij_typescript_field_prefix = _ 202 | ij_typescript_file_name_style = relaxed 203 | ij_typescript_finally_on_new_line = false 204 | ij_typescript_for_brace_force = never 205 | ij_typescript_for_statement_new_line_after_left_paren = false 206 | ij_typescript_for_statement_right_paren_on_new_line = false 207 | ij_typescript_for_statement_wrap = off 208 | ij_typescript_force_quote_style = false 209 | ij_typescript_force_semicolon_style = false 210 | ij_typescript_function_expression_brace_style = end_of_line 211 | ij_typescript_if_brace_force = never 212 | ij_typescript_import_merge_members = global 213 | ij_typescript_import_prefer_absolute_path = global 214 | ij_typescript_import_sort_members = true 215 | ij_typescript_import_sort_module_name = false 216 | ij_typescript_import_use_node_resolution = true 217 | ij_typescript_imports_wrap = on_every_item 218 | ij_typescript_indent_case_from_switch = true 219 | ij_typescript_indent_chained_calls = true 220 | ij_typescript_indent_package_children = 0 221 | ij_typescript_jsdoc_include_types = false 222 | ij_typescript_jsx_attribute_value = braces 223 | ij_typescript_keep_blank_lines_in_code = 2 224 | ij_typescript_keep_first_column_comment = true 225 | ij_typescript_keep_indents_on_empty_lines = false 226 | ij_typescript_keep_line_breaks = true 227 | ij_typescript_keep_simple_blocks_in_one_line = false 228 | ij_typescript_keep_simple_methods_in_one_line = false 229 | ij_typescript_line_comment_add_space = true 230 | ij_typescript_line_comment_at_first_column = false 231 | ij_typescript_method_brace_style = end_of_line 232 | ij_typescript_method_call_chain_wrap = off 233 | ij_typescript_method_parameters_new_line_after_left_paren = false 234 | ij_typescript_method_parameters_right_paren_on_new_line = false 235 | ij_typescript_method_parameters_wrap = off 236 | ij_typescript_object_literal_wrap = on_every_item 237 | ij_typescript_parentheses_expression_new_line_after_left_paren = false 238 | ij_typescript_parentheses_expression_right_paren_on_new_line = false 239 | ij_typescript_place_assignment_sign_on_next_line = false 240 | ij_typescript_prefer_as_type_cast = false 241 | ij_typescript_prefer_explicit_types_function_expression_returns = false 242 | ij_typescript_prefer_explicit_types_function_returns = false 243 | ij_typescript_prefer_explicit_types_vars_fields = false 244 | ij_typescript_prefer_parameters_wrap = false 245 | ij_typescript_reformat_c_style_comments = false 246 | ij_typescript_space_after_colon = true 247 | ij_typescript_space_after_comma = true 248 | ij_typescript_space_after_dots_in_rest_parameter = false 249 | ij_typescript_space_after_generator_mult = true 250 | ij_typescript_space_after_property_colon = true 251 | ij_typescript_space_after_quest = true 252 | ij_typescript_space_after_type_colon = true 253 | ij_typescript_space_after_unary_not = false 254 | ij_typescript_space_before_async_arrow_lparen = true 255 | ij_typescript_space_before_catch_keyword = true 256 | ij_typescript_space_before_catch_left_brace = true 257 | ij_typescript_space_before_catch_parentheses = true 258 | ij_typescript_space_before_class_lbrace = true 259 | ij_typescript_space_before_class_left_brace = true 260 | ij_typescript_space_before_colon = true 261 | ij_typescript_space_before_comma = false 262 | ij_typescript_space_before_do_left_brace = true 263 | ij_typescript_space_before_else_keyword = true 264 | ij_typescript_space_before_else_left_brace = true 265 | ij_typescript_space_before_finally_keyword = true 266 | ij_typescript_space_before_finally_left_brace = true 267 | ij_typescript_space_before_for_left_brace = true 268 | ij_typescript_space_before_for_parentheses = true 269 | ij_typescript_space_before_for_semicolon = false 270 | ij_typescript_space_before_function_left_parenth = true 271 | ij_typescript_space_before_generator_mult = false 272 | ij_typescript_space_before_if_left_brace = true 273 | ij_typescript_space_before_if_parentheses = true 274 | ij_typescript_space_before_method_call_parentheses = false 275 | ij_typescript_space_before_method_left_brace = true 276 | ij_typescript_space_before_method_parentheses = false 277 | ij_typescript_space_before_property_colon = false 278 | ij_typescript_space_before_quest = true 279 | ij_typescript_space_before_switch_left_brace = true 280 | ij_typescript_space_before_switch_parentheses = true 281 | ij_typescript_space_before_try_left_brace = true 282 | ij_typescript_space_before_type_colon = false 283 | ij_typescript_space_before_unary_not = false 284 | ij_typescript_space_before_while_keyword = true 285 | ij_typescript_space_before_while_left_brace = true 286 | ij_typescript_space_before_while_parentheses = true 287 | ij_typescript_spaces_around_additive_operators = true 288 | ij_typescript_spaces_around_arrow_function_operator = true 289 | ij_typescript_spaces_around_assignment_operators = true 290 | ij_typescript_spaces_around_bitwise_operators = true 291 | ij_typescript_spaces_around_equality_operators = true 292 | ij_typescript_spaces_around_logical_operators = true 293 | ij_typescript_spaces_around_multiplicative_operators = true 294 | ij_typescript_spaces_around_relational_operators = true 295 | ij_typescript_spaces_around_shift_operators = true 296 | ij_typescript_spaces_around_unary_operator = false 297 | ij_typescript_spaces_within_array_initializer_brackets = false 298 | ij_typescript_spaces_within_brackets = false 299 | ij_typescript_spaces_within_catch_parentheses = false 300 | ij_typescript_spaces_within_for_parentheses = false 301 | ij_typescript_spaces_within_if_parentheses = false 302 | ij_typescript_spaces_within_imports = false 303 | ij_typescript_spaces_within_interpolation_expressions = false 304 | ij_typescript_spaces_within_method_call_parentheses = false 305 | ij_typescript_spaces_within_method_parentheses = false 306 | ij_typescript_spaces_within_object_literal_braces = false 307 | ij_typescript_spaces_within_object_type_braces = true 308 | ij_typescript_spaces_within_parentheses = false 309 | ij_typescript_spaces_within_switch_parentheses = false 310 | ij_typescript_spaces_within_type_assertion = false 311 | ij_typescript_spaces_within_union_types = true 312 | ij_typescript_spaces_within_while_parentheses = false 313 | ij_typescript_special_else_if_treatment = true 314 | ij_typescript_ternary_operation_signs_on_next_line = false 315 | ij_typescript_ternary_operation_wrap = off 316 | ij_typescript_union_types_wrap = on_every_item 317 | ij_typescript_use_chained_calls_group_indents = false 318 | ij_typescript_use_double_quotes = true 319 | ij_typescript_use_explicit_js_extension = global 320 | ij_typescript_use_path_mapping = always 321 | ij_typescript_use_public_modifier = false 322 | ij_typescript_use_semicolon_after_statement = true 323 | ij_typescript_var_declaration_wrap = normal 324 | ij_typescript_while_brace_force = never 325 | ij_typescript_while_on_new_line = false 326 | ij_typescript_wrap_comments = false 327 | 328 | [{*.bash,*.sh,*.zsh}] 329 | indent_size = 2 330 | tab_width = 2 331 | ij_shell_binary_ops_start_line = false 332 | ij_shell_keep_column_alignment_padding = false 333 | ij_shell_minify_program = false 334 | ij_shell_redirect_followed_by_space = false 335 | ij_shell_switch_cases_indented = false 336 | ij_shell_use_unix_line_separator = true 337 | 338 | [{*.cjs,*.js}] 339 | indent_size = 2 340 | max_line_length = 80 341 | tab_width = 2 342 | ij_continuation_indent_size = 4 343 | ij_javascript_align_imports = false 344 | ij_javascript_align_multiline_array_initializer_expression = false 345 | ij_javascript_align_multiline_binary_operation = false 346 | ij_javascript_align_multiline_chained_methods = false 347 | ij_javascript_align_multiline_extends_list = false 348 | ij_javascript_align_multiline_for = false 349 | ij_javascript_align_multiline_parameters = false 350 | ij_javascript_align_multiline_parameters_in_calls = false 351 | ij_javascript_align_multiline_ternary_operation = false 352 | ij_javascript_align_object_properties = 0 353 | ij_javascript_align_union_types = false 354 | ij_javascript_align_var_statements = 0 355 | ij_javascript_array_initializer_new_line_after_left_brace = true 356 | ij_javascript_array_initializer_right_brace_on_new_line = false 357 | ij_javascript_array_initializer_wrap = on_every_item 358 | ij_javascript_assignment_wrap = off 359 | ij_javascript_binary_operation_sign_on_next_line = false 360 | ij_javascript_binary_operation_wrap = normal 361 | ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** 362 | ij_javascript_blank_lines_after_imports = 1 363 | ij_javascript_blank_lines_around_class = 1 364 | ij_javascript_blank_lines_around_field = 0 365 | ij_javascript_blank_lines_around_function = 1 366 | ij_javascript_blank_lines_around_method = 1 367 | ij_javascript_block_brace_style = end_of_line 368 | ij_javascript_call_parameters_new_line_after_left_paren = false 369 | ij_javascript_call_parameters_right_paren_on_new_line = false 370 | ij_javascript_call_parameters_wrap = normal 371 | ij_javascript_catch_on_new_line = false 372 | ij_javascript_chained_call_dot_on_new_line = false 373 | ij_javascript_class_brace_style = end_of_line 374 | ij_javascript_comma_on_new_line = false 375 | ij_javascript_do_while_brace_force = if_multiline 376 | ij_javascript_else_on_new_line = false 377 | ij_javascript_enforce_trailing_comma = whenmultiline 378 | ij_javascript_extends_keyword_wrap = off 379 | ij_javascript_extends_list_wrap = off 380 | ij_javascript_field_prefix = _ 381 | ij_javascript_file_name_style = relaxed 382 | ij_javascript_finally_on_new_line = false 383 | ij_javascript_for_brace_force = if_multiline 384 | ij_javascript_for_statement_new_line_after_left_paren = false 385 | ij_javascript_for_statement_right_paren_on_new_line = false 386 | ij_javascript_for_statement_wrap = off 387 | ij_javascript_force_quote_style = true 388 | ij_javascript_force_semicolon_style = true 389 | ij_javascript_function_expression_brace_style = end_of_line 390 | ij_javascript_if_brace_force = if_multiline 391 | ij_javascript_import_merge_members = global 392 | ij_javascript_import_prefer_absolute_path = global 393 | ij_javascript_import_sort_members = true 394 | ij_javascript_import_sort_module_name = false 395 | ij_javascript_import_use_node_resolution = true 396 | ij_javascript_imports_wrap = on_every_item 397 | ij_javascript_indent_case_from_switch = true 398 | ij_javascript_indent_chained_calls = true 399 | ij_javascript_indent_package_children = 0 400 | ij_javascript_jsx_attribute_value = braces 401 | ij_javascript_keep_blank_lines_in_code = 1 402 | ij_javascript_keep_first_column_comment = true 403 | ij_javascript_keep_indents_on_empty_lines = false 404 | ij_javascript_keep_line_breaks = true 405 | ij_javascript_keep_simple_blocks_in_one_line = false 406 | ij_javascript_keep_simple_methods_in_one_line = false 407 | ij_javascript_line_comment_add_space = true 408 | ij_javascript_line_comment_at_first_column = false 409 | ij_javascript_method_brace_style = end_of_line 410 | ij_javascript_method_call_chain_wrap = on_every_item 411 | ij_javascript_method_parameters_new_line_after_left_paren = true 412 | ij_javascript_method_parameters_right_paren_on_new_line = false 413 | ij_javascript_method_parameters_wrap = normal 414 | ij_javascript_object_literal_wrap = on_every_item 415 | ij_javascript_parentheses_expression_new_line_after_left_paren = false 416 | ij_javascript_parentheses_expression_right_paren_on_new_line = false 417 | ij_javascript_place_assignment_sign_on_next_line = false 418 | ij_javascript_prefer_as_type_cast = false 419 | ij_javascript_prefer_explicit_types_function_expression_returns = false 420 | ij_javascript_prefer_explicit_types_function_returns = false 421 | ij_javascript_prefer_explicit_types_vars_fields = false 422 | ij_javascript_prefer_parameters_wrap = false 423 | ij_javascript_reformat_c_style_comments = false 424 | ij_javascript_space_after_colon = true 425 | ij_javascript_space_after_comma = true 426 | ij_javascript_space_after_dots_in_rest_parameter = false 427 | ij_javascript_space_after_generator_mult = true 428 | ij_javascript_space_after_property_colon = true 429 | ij_javascript_space_after_quest = true 430 | ij_javascript_space_after_type_colon = true 431 | ij_javascript_space_after_unary_not = false 432 | ij_javascript_space_before_async_arrow_lparen = true 433 | ij_javascript_space_before_catch_keyword = true 434 | ij_javascript_space_before_catch_left_brace = true 435 | ij_javascript_space_before_catch_parentheses = true 436 | ij_javascript_space_before_class_lbrace = true 437 | ij_javascript_space_before_class_left_brace = true 438 | ij_javascript_space_before_colon = true 439 | ij_javascript_space_before_comma = false 440 | ij_javascript_space_before_do_left_brace = true 441 | ij_javascript_space_before_else_keyword = true 442 | ij_javascript_space_before_else_left_brace = true 443 | ij_javascript_space_before_finally_keyword = true 444 | ij_javascript_space_before_finally_left_brace = true 445 | ij_javascript_space_before_for_left_brace = true 446 | ij_javascript_space_before_for_parentheses = true 447 | ij_javascript_space_before_for_semicolon = false 448 | ij_javascript_space_before_function_left_parenth = false 449 | ij_javascript_space_before_generator_mult = false 450 | ij_javascript_space_before_if_left_brace = true 451 | ij_javascript_space_before_if_parentheses = true 452 | ij_javascript_space_before_method_call_parentheses = false 453 | ij_javascript_space_before_method_left_brace = true 454 | ij_javascript_space_before_method_parentheses = false 455 | ij_javascript_space_before_property_colon = false 456 | ij_javascript_space_before_quest = true 457 | ij_javascript_space_before_switch_left_brace = true 458 | ij_javascript_space_before_switch_parentheses = true 459 | ij_javascript_space_before_try_left_brace = true 460 | ij_javascript_space_before_type_colon = false 461 | ij_javascript_space_before_unary_not = false 462 | ij_javascript_space_before_while_keyword = true 463 | ij_javascript_space_before_while_left_brace = true 464 | ij_javascript_space_before_while_parentheses = true 465 | ij_javascript_spaces_around_additive_operators = true 466 | ij_javascript_spaces_around_arrow_function_operator = true 467 | ij_javascript_spaces_around_assignment_operators = true 468 | ij_javascript_spaces_around_bitwise_operators = true 469 | ij_javascript_spaces_around_equality_operators = true 470 | ij_javascript_spaces_around_logical_operators = true 471 | ij_javascript_spaces_around_multiplicative_operators = true 472 | ij_javascript_spaces_around_relational_operators = true 473 | ij_javascript_spaces_around_shift_operators = true 474 | ij_javascript_spaces_around_unary_operator = false 475 | ij_javascript_spaces_within_array_initializer_brackets = false 476 | ij_javascript_spaces_within_brackets = false 477 | ij_javascript_spaces_within_catch_parentheses = false 478 | ij_javascript_spaces_within_for_parentheses = false 479 | ij_javascript_spaces_within_if_parentheses = false 480 | ij_javascript_spaces_within_imports = false 481 | ij_javascript_spaces_within_interpolation_expressions = false 482 | ij_javascript_spaces_within_method_call_parentheses = false 483 | ij_javascript_spaces_within_method_parentheses = false 484 | ij_javascript_spaces_within_object_literal_braces = false 485 | ij_javascript_spaces_within_object_type_braces = true 486 | ij_javascript_spaces_within_parentheses = false 487 | ij_javascript_spaces_within_switch_parentheses = false 488 | ij_javascript_spaces_within_type_assertion = false 489 | ij_javascript_spaces_within_union_types = true 490 | ij_javascript_spaces_within_while_parentheses = false 491 | ij_javascript_special_else_if_treatment = true 492 | ij_javascript_ternary_operation_signs_on_next_line = false 493 | ij_javascript_ternary_operation_wrap = on_every_item 494 | ij_javascript_union_types_wrap = on_every_item 495 | ij_javascript_use_chained_calls_group_indents = false 496 | ij_javascript_use_double_quotes = false 497 | ij_javascript_use_explicit_js_extension = global 498 | ij_javascript_use_path_mapping = always 499 | ij_javascript_use_public_modifier = false 500 | ij_javascript_use_semicolon_after_statement = true 501 | ij_javascript_var_declaration_wrap = normal 502 | ij_javascript_while_brace_force = if_multiline 503 | ij_javascript_while_on_new_line = false 504 | ij_javascript_wrap_comments = false 505 | 506 | [{*.cjsx,*.coffee}] 507 | indent_size = 2 508 | tab_width = 2 509 | ij_continuation_indent_size = 2 510 | ij_visual_guides = none 511 | ij_coffeescript_align_function_body = false 512 | ij_coffeescript_align_imports = false 513 | ij_coffeescript_align_multiline_array_initializer_expression = true 514 | ij_coffeescript_align_multiline_parameters = true 515 | ij_coffeescript_align_multiline_parameters_in_calls = false 516 | ij_coffeescript_align_object_properties = 0 517 | ij_coffeescript_align_union_types = false 518 | ij_coffeescript_align_var_statements = 0 519 | ij_coffeescript_array_initializer_new_line_after_left_brace = false 520 | ij_coffeescript_array_initializer_right_brace_on_new_line = false 521 | ij_coffeescript_array_initializer_wrap = normal 522 | ij_coffeescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** 523 | ij_coffeescript_blank_lines_around_function = 1 524 | ij_coffeescript_call_parameters_new_line_after_left_paren = false 525 | ij_coffeescript_call_parameters_right_paren_on_new_line = false 526 | ij_coffeescript_call_parameters_wrap = normal 527 | ij_coffeescript_chained_call_dot_on_new_line = true 528 | ij_coffeescript_comma_on_new_line = false 529 | ij_coffeescript_enforce_trailing_comma = keep 530 | ij_coffeescript_field_prefix = _ 531 | ij_coffeescript_file_name_style = relaxed 532 | ij_coffeescript_force_quote_style = false 533 | ij_coffeescript_force_semicolon_style = false 534 | ij_coffeescript_function_expression_brace_style = end_of_line 535 | ij_coffeescript_import_merge_members = global 536 | ij_coffeescript_import_prefer_absolute_path = global 537 | ij_coffeescript_import_sort_members = true 538 | ij_coffeescript_import_sort_module_name = false 539 | ij_coffeescript_import_use_node_resolution = true 540 | ij_coffeescript_imports_wrap = on_every_item 541 | ij_coffeescript_indent_chained_calls = true 542 | ij_coffeescript_indent_package_children = 0 543 | ij_coffeescript_jsx_attribute_value = braces 544 | ij_coffeescript_keep_blank_lines_in_code = 2 545 | ij_coffeescript_keep_first_column_comment = true 546 | ij_coffeescript_keep_indents_on_empty_lines = false 547 | ij_coffeescript_keep_line_breaks = true 548 | ij_coffeescript_keep_simple_methods_in_one_line = false 549 | ij_coffeescript_method_parameters_new_line_after_left_paren = false 550 | ij_coffeescript_method_parameters_right_paren_on_new_line = false 551 | ij_coffeescript_method_parameters_wrap = off 552 | ij_coffeescript_object_literal_wrap = on_every_item 553 | ij_coffeescript_prefer_as_type_cast = false 554 | ij_coffeescript_prefer_explicit_types_function_expression_returns = false 555 | ij_coffeescript_prefer_explicit_types_function_returns = false 556 | ij_coffeescript_prefer_explicit_types_vars_fields = false 557 | ij_coffeescript_reformat_c_style_comments = false 558 | ij_coffeescript_space_after_comma = true 559 | ij_coffeescript_space_after_dots_in_rest_parameter = false 560 | ij_coffeescript_space_after_generator_mult = true 561 | ij_coffeescript_space_after_property_colon = true 562 | ij_coffeescript_space_after_type_colon = true 563 | ij_coffeescript_space_after_unary_not = false 564 | ij_coffeescript_space_before_async_arrow_lparen = true 565 | ij_coffeescript_space_before_class_lbrace = true 566 | ij_coffeescript_space_before_comma = false 567 | ij_coffeescript_space_before_function_left_parenth = true 568 | ij_coffeescript_space_before_generator_mult = false 569 | ij_coffeescript_space_before_property_colon = false 570 | ij_coffeescript_space_before_type_colon = false 571 | ij_coffeescript_space_before_unary_not = false 572 | ij_coffeescript_spaces_around_additive_operators = true 573 | ij_coffeescript_spaces_around_arrow_function_operator = true 574 | ij_coffeescript_spaces_around_assignment_operators = true 575 | ij_coffeescript_spaces_around_bitwise_operators = true 576 | ij_coffeescript_spaces_around_equality_operators = true 577 | ij_coffeescript_spaces_around_logical_operators = true 578 | ij_coffeescript_spaces_around_multiplicative_operators = true 579 | ij_coffeescript_spaces_around_relational_operators = true 580 | ij_coffeescript_spaces_around_shift_operators = true 581 | ij_coffeescript_spaces_around_unary_operator = false 582 | ij_coffeescript_spaces_within_array_initializer_braces = false 583 | ij_coffeescript_spaces_within_array_initializer_brackets = false 584 | ij_coffeescript_spaces_within_imports = false 585 | ij_coffeescript_spaces_within_index_brackets = false 586 | ij_coffeescript_spaces_within_interpolation_expressions = false 587 | ij_coffeescript_spaces_within_method_call_parentheses = false 588 | ij_coffeescript_spaces_within_method_parentheses = false 589 | ij_coffeescript_spaces_within_object_braces = false 590 | ij_coffeescript_spaces_within_object_literal_braces = false 591 | ij_coffeescript_spaces_within_object_type_braces = true 592 | ij_coffeescript_spaces_within_range_brackets = false 593 | ij_coffeescript_spaces_within_type_assertion = false 594 | ij_coffeescript_spaces_within_union_types = true 595 | ij_coffeescript_union_types_wrap = on_every_item 596 | ij_coffeescript_use_chained_calls_group_indents = false 597 | ij_coffeescript_use_double_quotes = true 598 | ij_coffeescript_use_explicit_js_extension = global 599 | ij_coffeescript_use_path_mapping = always 600 | ij_coffeescript_use_public_modifier = false 601 | ij_coffeescript_use_semicolon_after_statement = false 602 | ij_coffeescript_var_declaration_wrap = normal 603 | 604 | [{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}] 605 | ij_continuation_indent_size = 4 606 | ij_php_align_assignments = false 607 | ij_php_align_class_constants = false 608 | ij_php_align_group_field_declarations = false 609 | ij_php_align_inline_comments = false 610 | ij_php_align_key_value_pairs = false 611 | ij_php_align_match_arm_bodies = false 612 | ij_php_align_multiline_array_initializer_expression = false 613 | ij_php_align_multiline_binary_operation = false 614 | ij_php_align_multiline_chained_methods = false 615 | ij_php_align_multiline_extends_list = false 616 | ij_php_align_multiline_for = true 617 | ij_php_align_multiline_parameters = false 618 | ij_php_align_multiline_parameters_in_calls = false 619 | ij_php_align_multiline_ternary_operation = false 620 | ij_php_align_named_arguments = false 621 | ij_php_align_phpdoc_comments = false 622 | ij_php_align_phpdoc_param_names = false 623 | ij_php_anonymous_brace_style = end_of_line 624 | ij_php_api_weight = 28 625 | ij_php_array_initializer_new_line_after_left_brace = true 626 | ij_php_array_initializer_right_brace_on_new_line = true 627 | ij_php_array_initializer_wrap = on_every_item 628 | ij_php_assignment_wrap = off 629 | ij_php_attributes_wrap = off 630 | ij_php_author_weight = 28 631 | ij_php_binary_operation_sign_on_next_line = false 632 | ij_php_binary_operation_wrap = off 633 | ij_php_blank_lines_after_class_header = 0 634 | ij_php_blank_lines_after_function = 1 635 | ij_php_blank_lines_after_imports = 1 636 | ij_php_blank_lines_after_opening_tag = 1 637 | ij_php_blank_lines_after_package = 1 638 | ij_php_blank_lines_around_class = 1 639 | ij_php_blank_lines_around_constants = 1 640 | ij_php_blank_lines_around_field = 1 641 | ij_php_blank_lines_around_method = 1 642 | ij_php_blank_lines_before_class_end = 0 643 | ij_php_blank_lines_before_imports = 1 644 | ij_php_blank_lines_before_method_body = 0 645 | ij_php_blank_lines_before_package = 1 646 | ij_php_blank_lines_before_return_statement = 0 647 | ij_php_blank_lines_between_imports = 1 648 | ij_php_block_brace_style = end_of_line 649 | ij_php_call_parameters_new_line_after_left_paren = true 650 | ij_php_call_parameters_right_paren_on_new_line = true 651 | ij_php_call_parameters_wrap = on_every_item 652 | ij_php_catch_on_new_line = false 653 | ij_php_category_weight = 28 654 | ij_php_class_brace_style = next_line 655 | ij_php_comma_after_last_array_element = true 656 | ij_php_concat_spaces = true 657 | ij_php_copyright_weight = 28 658 | ij_php_deprecated_weight = 28 659 | ij_php_do_while_brace_force = never 660 | ij_php_else_if_style = separate 661 | ij_php_else_on_new_line = false 662 | ij_php_example_weight = 28 663 | ij_php_extends_keyword_wrap = off 664 | ij_php_extends_list_wrap = off 665 | ij_php_fields_default_visibility = private 666 | ij_php_filesource_weight = 28 667 | ij_php_finally_on_new_line = false 668 | ij_php_for_brace_force = never 669 | ij_php_for_statement_new_line_after_left_paren = false 670 | ij_php_for_statement_right_paren_on_new_line = false 671 | ij_php_for_statement_wrap = off 672 | ij_php_force_short_declaration_array_style = true 673 | ij_php_getters_setters_naming_style = camel_case 674 | ij_php_getters_setters_order_style = getters_first 675 | ij_php_global_weight = 28 676 | ij_php_group_use_wrap = on_every_item 677 | ij_php_if_brace_force = always 678 | ij_php_if_lparen_on_next_line = true 679 | ij_php_if_rparen_on_next_line = true 680 | ij_php_ignore_weight = 28 681 | ij_php_import_sorting = alphabetic 682 | ij_php_indent_break_from_case = true 683 | ij_php_indent_case_from_switch = true 684 | ij_php_indent_code_in_php_tags = false 685 | ij_php_internal_weight = 28 686 | ij_php_keep_blank_lines_after_lbrace = 2 687 | ij_php_keep_blank_lines_before_right_brace = 2 688 | ij_php_keep_blank_lines_in_code = 2 689 | ij_php_keep_blank_lines_in_declarations = 2 690 | ij_php_keep_control_statement_in_one_line = true 691 | ij_php_keep_first_column_comment = true 692 | ij_php_keep_indents_on_empty_lines = false 693 | ij_php_keep_line_breaks = true 694 | ij_php_keep_rparen_and_lbrace_on_one_line = true 695 | ij_php_keep_simple_classes_in_one_line = true 696 | ij_php_keep_simple_methods_in_one_line = true 697 | ij_php_lambda_brace_style = end_of_line 698 | ij_php_license_weight = 28 699 | ij_php_line_comment_add_space = false 700 | ij_php_line_comment_at_first_column = true 701 | ij_php_link_weight = 28 702 | ij_php_lower_case_boolean_const = true 703 | ij_php_lower_case_keywords = true 704 | ij_php_lower_case_null_const = true 705 | ij_php_method_brace_style = next_line 706 | ij_php_method_call_chain_wrap = on_every_item 707 | ij_php_method_parameters_new_line_after_left_paren = true 708 | ij_php_method_parameters_right_paren_on_new_line = true 709 | ij_php_method_parameters_wrap = on_every_item 710 | ij_php_method_weight = 28 711 | ij_php_modifier_list_wrap = false 712 | ij_php_multiline_chained_calls_semicolon_on_new_line = false 713 | ij_php_namespace_brace_style = 1 714 | ij_php_new_line_after_php_opening_tag = true 715 | ij_php_null_type_position = in_the_end 716 | ij_php_package_weight = 28 717 | ij_php_param_weight = 0 718 | ij_php_parameters_attributes_wrap = off 719 | ij_php_parentheses_expression_new_line_after_left_paren = false 720 | ij_php_parentheses_expression_right_paren_on_new_line = false 721 | ij_php_phpdoc_blank_line_before_tags = true 722 | ij_php_phpdoc_blank_lines_around_parameters = true 723 | ij_php_phpdoc_keep_blank_lines = true 724 | ij_php_phpdoc_param_spaces_between_name_and_description = 1 725 | ij_php_phpdoc_param_spaces_between_tag_and_type = 1 726 | ij_php_phpdoc_param_spaces_between_type_and_name = 1 727 | ij_php_phpdoc_use_fqcn = false 728 | ij_php_phpdoc_wrap_long_lines = true 729 | ij_php_place_assignment_sign_on_next_line = false 730 | ij_php_place_parens_for_constructor = 0 731 | ij_php_property_read_weight = 28 732 | ij_php_property_weight = 28 733 | ij_php_property_write_weight = 28 734 | ij_php_return_type_on_new_line = false 735 | ij_php_return_weight = 1 736 | ij_php_see_weight = 28 737 | ij_php_since_weight = 28 738 | ij_php_sort_phpdoc_elements = true 739 | ij_php_space_after_colon = true 740 | ij_php_space_after_colon_in_enum_backed_type = true 741 | ij_php_space_after_colon_in_named_argument = true 742 | ij_php_space_after_colon_in_return_type = true 743 | ij_php_space_after_comma = true 744 | ij_php_space_after_for_semicolon = true 745 | ij_php_space_after_quest = true 746 | ij_php_space_after_type_cast = true 747 | ij_php_space_after_unary_not = false 748 | ij_php_space_before_array_initializer_left_brace = false 749 | ij_php_space_before_catch_keyword = true 750 | ij_php_space_before_catch_left_brace = true 751 | ij_php_space_before_catch_parentheses = true 752 | ij_php_space_before_class_left_brace = true 753 | ij_php_space_before_closure_left_parenthesis = true 754 | ij_php_space_before_colon = true 755 | ij_php_space_before_colon_in_enum_backed_type = false 756 | ij_php_space_before_colon_in_named_argument = false 757 | ij_php_space_before_colon_in_return_type = false 758 | ij_php_space_before_comma = false 759 | ij_php_space_before_do_left_brace = true 760 | ij_php_space_before_else_keyword = true 761 | ij_php_space_before_else_left_brace = true 762 | ij_php_space_before_finally_keyword = true 763 | ij_php_space_before_finally_left_brace = true 764 | ij_php_space_before_for_left_brace = true 765 | ij_php_space_before_for_parentheses = true 766 | ij_php_space_before_for_semicolon = false 767 | ij_php_space_before_if_left_brace = true 768 | ij_php_space_before_if_parentheses = true 769 | ij_php_space_before_method_call_parentheses = false 770 | ij_php_space_before_method_left_brace = true 771 | ij_php_space_before_method_parentheses = false 772 | ij_php_space_before_quest = true 773 | ij_php_space_before_short_closure_left_parenthesis = true 774 | ij_php_space_before_switch_left_brace = true 775 | ij_php_space_before_switch_parentheses = true 776 | ij_php_space_before_try_left_brace = true 777 | ij_php_space_before_unary_not = false 778 | ij_php_space_before_while_keyword = true 779 | ij_php_space_before_while_left_brace = true 780 | ij_php_space_before_while_parentheses = true 781 | ij_php_space_between_ternary_quest_and_colon = false 782 | ij_php_spaces_around_additive_operators = true 783 | ij_php_spaces_around_arrow = false 784 | ij_php_spaces_around_assignment_in_declare = false 785 | ij_php_spaces_around_assignment_operators = true 786 | ij_php_spaces_around_bitwise_operators = true 787 | ij_php_spaces_around_equality_operators = true 788 | ij_php_spaces_around_logical_operators = true 789 | ij_php_spaces_around_multiplicative_operators = true 790 | ij_php_spaces_around_null_coalesce_operator = true 791 | ij_php_spaces_around_pipe_in_union_type = false 792 | ij_php_spaces_around_relational_operators = true 793 | ij_php_spaces_around_shift_operators = true 794 | ij_php_spaces_around_unary_operator = false 795 | ij_php_spaces_around_var_within_brackets = false 796 | ij_php_spaces_within_array_initializer_braces = false 797 | ij_php_spaces_within_brackets = false 798 | ij_php_spaces_within_catch_parentheses = false 799 | ij_php_spaces_within_for_parentheses = false 800 | ij_php_spaces_within_if_parentheses = false 801 | ij_php_spaces_within_method_call_parentheses = false 802 | ij_php_spaces_within_method_parentheses = false 803 | ij_php_spaces_within_parentheses = false 804 | ij_php_spaces_within_short_echo_tags = true 805 | ij_php_spaces_within_switch_parentheses = false 806 | ij_php_spaces_within_while_parentheses = false 807 | ij_php_special_else_if_treatment = false 808 | ij_php_subpackage_weight = 28 809 | ij_php_ternary_operation_signs_on_next_line = false 810 | ij_php_ternary_operation_wrap = on_every_item 811 | ij_php_throws_weight = 2 812 | ij_php_todo_weight = 28 813 | ij_php_unknown_tag_weight = 28 814 | ij_php_upper_case_boolean_const = false 815 | ij_php_upper_case_null_const = false 816 | ij_php_uses_weight = 28 817 | ij_php_var_weight = 28 818 | ij_php_variable_naming_style = camel_case 819 | ij_php_version_weight = 28 820 | ij_php_while_brace_force = never 821 | ij_php_while_on_new_line = false 822 | 823 | [{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,composer.lock,jest.config}] 824 | indent_size = 2 825 | ij_visual_guides = none 826 | ij_json_keep_blank_lines_in_code = 0 827 | ij_json_keep_indents_on_empty_lines = false 828 | ij_json_keep_line_breaks = true 829 | ij_json_space_after_colon = true 830 | ij_json_space_after_comma = true 831 | ij_json_space_before_colon = true 832 | ij_json_space_before_comma = false 833 | ij_json_spaces_within_braces = false 834 | ij_json_spaces_within_brackets = false 835 | ij_json_wrap_long_lines = false 836 | 837 | [{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] 838 | ij_continuation_indent_size = 2 839 | ij_visual_guides = none 840 | ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 841 | ij_html_align_attributes = true 842 | ij_html_align_text = false 843 | ij_html_attribute_wrap = normal 844 | ij_html_block_comment_at_first_column = true 845 | ij_html_do_not_align_children_of_min_lines = 0 846 | ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p 847 | ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot 848 | ij_html_enforce_quotes = true 849 | ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var 850 | ij_html_keep_blank_lines = 2 851 | ij_html_keep_indents_on_empty_lines = false 852 | ij_html_keep_line_breaks = true 853 | ij_html_keep_line_breaks_in_text = true 854 | ij_html_keep_whitespaces = false 855 | ij_html_keep_whitespaces_inside = span,pre,textarea 856 | ij_html_line_comment_at_first_column = true 857 | ij_html_new_line_after_last_attribute = when multiline 858 | ij_html_new_line_before_first_attribute = never 859 | ij_html_quote_style = double 860 | ij_html_remove_new_line_before_tags = br 861 | ij_html_space_after_tag_name = false 862 | ij_html_space_around_equality_in_attribute = false 863 | ij_html_space_inside_empty_tag = false 864 | ij_html_text_wrap = normal 865 | 866 | [{*.markdown,*.md}] 867 | ij_visual_guides = none 868 | ij_markdown_force_one_space_after_blockquote_symbol = true 869 | ij_markdown_force_one_space_after_header_symbol = true 870 | ij_markdown_force_one_space_after_list_bullet = true 871 | ij_markdown_force_one_space_between_words = true 872 | ij_markdown_keep_indents_on_empty_lines = false 873 | ij_markdown_max_lines_around_block_elements = 1 874 | ij_markdown_max_lines_around_header = 1 875 | ij_markdown_max_lines_between_paragraphs = 1 876 | ij_markdown_min_lines_around_block_elements = 1 877 | ij_markdown_min_lines_around_header = 1 878 | ij_markdown_min_lines_between_paragraphs = 1 879 | 880 | [{*.yaml,*.yml,*.config}] 881 | indent_size = 2 882 | ij_visual_guides = none 883 | ij_yaml_align_values_properties = do_not_align 884 | ij_yaml_autoinsert_sequence_marker = true 885 | ij_yaml_block_mapping_on_new_line = false 886 | ij_yaml_indent_sequence_value = true 887 | ij_yaml_keep_indents_on_empty_lines = false 888 | ij_yaml_keep_line_breaks = true 889 | ij_yaml_sequence_on_new_line = false 890 | ij_yaml_space_before_colon = false 891 | ij_yaml_spaces_within_braces = true 892 | ij_yaml_spaces_within_brackets = true 893 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .php_cs 3 | .php_cs.cache 4 | .phpunit.result.cache 5 | build 6 | composer.lock 7 | coverage 8 | vendor 9 | node_modules 10 | .php-cs-fixer.cache 11 | .phpactor.json 12 | laradumps.yaml 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.0 - 2025-03-01 4 | 5 | - Added support for Laravel 12. 6 | 7 | ## 1.1.0 - 2024-09-14 8 | 9 | - Key properties have been removed from serialised data. 10 | 11 | ## 1.0.0 - 2024-07-31 12 | 13 | - Initial release. 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2022 George Palmer, Honeystone Consulting Ltd 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 | # Honeystone DTO Tools for Laravel 2 | 3 | ![Static Badge](https://img.shields.io/badge/tests-passing-green) 4 | ![GitHub License](https://img.shields.io/github/license/honeystone/laravel-dto-tools) 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/honeystone/laravel-dto-tools)](https://packagist.org/packages/honeystone/laravel-dto-tools) 6 | ![Packagist Dependency Version](https://img.shields.io/packagist/dependency-v/honeystone/laravel-dto-tools/php) 7 | ![Packagist Dependency Version](https://img.shields.io/packagist/dependency-v/honeystone/laravel-dto-tools/illuminate%2Fcontracts?label=laravel) 8 | [![Static Badge](https://img.shields.io/badge/honeystone-fa6900)](https://honeystone.com) 9 | 10 | DTO tools is a package designed to bring additional power and convenience to your native PHP data transfer objects. The 11 | main motivation for this package was to remove much of the boilerplate created moving data in to and out of DTOs. For 12 | example, transforming snake-cased model attributes to camel-cased to be consumed by your presentation layer, or casting 13 | a user inputted numerical string (after validation ofc) to an integer. 14 | 15 | Features include property casting and mutation, serialization, patch data handling, relationships, and model and 16 | collection transformation. 17 | 18 | ## Support us 19 | 20 | [![Support Us](https://honeystone.com/images/github/support-us.webp)](https://honeystone.com) 21 | 22 | We are committed to delivering high-quality open source packages maintained by the team at Honeystone. If you would 23 | like to support our efforts, simply use our packages, recommend them and contribute. 24 | 25 | If you need any help with your project, or require any custom development, please [get in touch](https://honeystone.com/contact-us). 26 | 27 | ## Installation 28 | 29 | ```shell 30 | composer require honeystone/laravel-dto-tools 31 | ``` 32 | 33 | Publish the configuration file with: 34 | 35 | ```shell 36 | php artisan vendor:publish --tag=honeystone-dto-tools-config 37 | ``` 38 | 39 | ## Usage 40 | 41 | This package requires very little modification to your existing DTOs. As a minimum, your DTOs need to implement the 42 | `Transferable` contract and use the `HasTransferableData` trait, like so: 43 | 44 | ```php 45 | toIso8601String(), 78 | ); 79 | ``` 80 | 81 | This is really messy, so we'll clean it up in the next section. 82 | 83 | ### Casting properties 84 | 85 | Casters are used to intercept and cast/mutate values before instantiating the DTO. They are implemented using PHP 86 | Attributes. In the following example, we'll ensure that our empty description is cast to `null`, that the status is cast 87 | to an enum, and that the modified time is represented in an iso-8601 string: 88 | 89 | ```php 90 | use App\Domains\Articles\Data\Enums\Status; 91 | use Honeystone\DtoTools\Casters\DateTimeCaster; 92 | use Honeystone\DtoTools\Casters\EnumCaster; 93 | use Honeystone\DtoTools\Casters\NullCaster; 94 | 95 | final readonly class ArticleData implements Transferable 96 | { 97 | use HasTransferableData; 98 | 99 | public function __construct( 100 | public string $title, 101 | 102 | #[NullCaster('')] 103 | public ?string $description, 104 | 105 | #[EnumCaster(Status::class)] 106 | public Status $status, 107 | 108 | #[DateTimeCaster] 109 | public string $modified, 110 | ) { 111 | } 112 | } 113 | ``` 114 | 115 | Our data can be a little looser now, but we still benefit from the DTO's type safety: 116 | 117 | ```php 118 | $data = ArticleData::make( 119 | title: $source['title'], 120 | description: $source['description'], 121 | status: $source['status'], 122 | modified: $source['modified'], 123 | ); 124 | 125 | //or even cleaner 126 | $data = ArticleData::make(Arr::only($source, 'title', 'description', 'status', 'modified')); 127 | ``` 128 | 129 | Much better. The following casters are available, or you can create your own: 130 | 131 | ```php 132 | #[DateTimeCaster('Y-m-d')] //takes a format string, or defaults to iso-8601 133 | #[EnumCaster(Status::class, State::class)] //takes one or more enum class strings 134 | #[NullCaster('', '-')] //takes one or more values that should be converted to null 135 | #[ScalarCaster('bool', 'null')] //takes one or more accepted types, if no types match it will cast to the last type 136 | ``` 137 | 138 | To implement your own casters, just create an `Attribute` that implements 139 | `Honeystone\DtoTools\Casters\Contracts\CastsValues`. 140 | 141 | ```php 142 | "🔥 $value 🔥", $parameters); 188 | } 189 | } 190 | ``` 191 | 192 | ```php 193 | echo SomeData::make(['foo', 'bar'])->getAttributes(); //['foo' => '🔥 foo 🔥', 'bar' => '🔥 bar 🔥'] 194 | ``` 195 | 196 | The `transformOutgoing()` method can also be implemented for DTO serialization: 197 | 198 | ```php 199 | final readonly class SomeData implements Transferable 200 | { 201 | use HasTransferableData 202 | 203 | public function __construct( 204 | public string $foo, 205 | public string $bar, 206 | ) { 207 | } 208 | 209 | protected static function transformOutgoing(array $parameters): array 210 | { 211 | return array_map(static fn (string $value): string => "🔥 $value 🔥", $parameters); 212 | } 213 | } 214 | ``` 215 | 216 | ```php 217 | echo SomeData::make(['foo', 'bar'])->getAttributes(); //['foo' => 'foo', 'bar' => 'bar'] 218 | echo SomeData::make(['foo', 'bar'])->toArray(); //['foo' => '🔥 foo 🔥', 'bar' => '🔥 bar 🔥'] 219 | ``` 220 | 221 | A very common use-case in Laravel will be converting snake-case properties to camel-case. As such, the 222 | `CreatableFromSnake` and `SerializesToSnake` traits are available: 223 | 224 | ```php 225 | use Honeystone\DtoTools\Concerns\CreatableFromSnake; 226 | use Honeystone\DtoTools\Concerns\SerializesToSnake; 227 | 228 | final readonly class SomeData implements Transferable 229 | { 230 | use HasTransferableData, CreatableFromSnake, SerializesToSnake; 231 | 232 | public function __construct( 233 | public string $someProperty, 234 | ) { 235 | } 236 | } 237 | ``` 238 | 239 | ```php 240 | echo SomeData::make(some_property: 'value')->getAttributes(); //['someProperty' => 'value'] 241 | echo SomeData::make(some_property: 'value')->toArray(); //['some_property' => 'value'] 242 | ``` 243 | 244 | ### Storable data 245 | 246 | We've looked at `Transferable` data so far, which is great for passing complete data through your service layer, but 247 | what if you need to process partial data (i.e. a patch)? This is `Storable` enters the scene. 248 | 249 | ```php 250 | toStorableArray(); //['foo' => 'value'] 302 | echo SomeData::make(foo: 'value', bar: null)->toArray(); //['foo' => 'value', 'bar => null] 303 | ``` 304 | 305 | You can also use the `isStorable()` method to check if an individual property can be stored: 306 | 307 | ```php 308 | $data = echo SomeData::make(foo: 'value', bar: null); 309 | 310 | $data->isStorable('foo'); //true 311 | $data->isStorable('bar'); //false 312 | ``` 313 | 314 | Sometimes, `null` is a valid value and should be stored. In these cases you can use the `force()` method to mark these 315 | properties as storable: 316 | 317 | ```php 318 | $data = echo SomeData::make(foo: 'value', bar: null); 319 | 320 | $data->force('bar'); 321 | 322 | $data->isStorable('bar'); //true 323 | ``` 324 | 325 | If you need a list of forced properties, use `getForced()`. 326 | 327 | ### Storable relationships 328 | 329 | Occasionally, when transferring data into your services layer you need to represent changes to relational structures. 330 | You could do this with a simple property on your DTO, for example: 331 | 332 | ```php 333 | |null $tags 341 | */ 342 | public function __construct( 343 | //... 344 | public ?array $tags = null, 345 | ) { 346 | } 347 | } 348 | ``` 349 | 350 | There are a few problems with this approach though: there's no real type safety, you can't just add or remove a tag, 351 | you have to provide all the tags, and you cant have any meta data (e.g. order). Maybe you upgrade this to be an array 352 | of DTOs, but there's an easier way: 353 | 354 | ```php 355 | 'int|empty'])] 360 | final class ArticlePatchData implements Storable 361 | { 362 | use HasStorableData; 363 | 364 | public function __construct( 365 | //... 366 | ) { 367 | } 368 | } 369 | ``` 370 | 371 | Using the `ToMany` class attribute we can declare a to-many relationship called 'tags' that can be empty, or integers. 372 | 373 | We can now add, remove and replace related tags: 374 | 375 | ```php 376 | $data = ArticlePatchData::make(...); 377 | 378 | $data->relationships()->addToManyRelation('foo', 123, ['priority' => 5]); 379 | ``` 380 | 381 | Relationships are stored using the related id. This can be either an integer or a string. You can also provide a 382 | `Transferable` or `Storable` and the library will use its `getKey()` method to determine the id. Additional meta data 383 | can be provided as an array or as a `Transferable`: 384 | 385 | ```php 386 | $data = ArticlePatchData::make(...); 387 | 388 | $data->relationships()->addToManyRelation('foo', TagData::make(...), TagMetaData::make(...)); 389 | ``` 390 | 391 | The following relationship class `Attributes` are supported: 392 | 393 | ```php 394 | #[ToOne(['foo' => 'int|string|null'])] 395 | #[ToMany(['bar' => 'int|string|empty'])] 396 | ``` 397 | 398 | And the following relationship methods are available: 399 | 400 | ```php 401 | $data->relationships()->hasToOne('foo'); 402 | $data->relationships()->getOneRelated('foo'); 403 | $data->relationships()->setOneRelated('foo', 123); 404 | $data->relationships()->unsetOneRelated('foo'); 405 | 406 | $data->relationships()->hasToMany('bar'); 407 | $data->relationships()->getManyRelated('bar'); //plus any additions, minus any removals 408 | $data->relationships()->getManyAdditions('bar'); 409 | $data->relationships()->getManyRemovals('bar'); 410 | $data->relationships()->addToManyRelation('bar', 123, []); //param 3 optional 411 | $data->relationships()->removeToManyRelation('bar', 123); 412 | $data->relationships()->replaceToMany('bar', 123, []); //param 3 optional, clears addition and removals 413 | $data->relationships()->resetToMany('bar'); //clears addition and removals 414 | $data->relationships()->getMetaData('bar'); 415 | $data->relationships()->setMetaData('bar', []); 416 | ``` 417 | 418 | Serialized relationships are included in `toStorableArray()`, or you can grab just the relationships with 419 | `getRelationships()`. 420 | 421 | ### Model transformation 422 | 423 | It's not uncommon to convert a `Model` into a DTO. They'll be different though. The data of a DTO should be more 424 | specific and situational. This can lead to a lot of boilerplate to handle the transformations. This package includes an 425 | `abstract` `ModelTransformer` to help clean this up. 426 | 427 | Here's the most basic example: 428 | 429 | ```php 430 | transform(Article::first()); 452 | $transformer->transformCollection(Article::all()); 453 | ``` 454 | 455 | Internally this example will use the `toArray()` method of your model. 456 | 457 | We can be specific about which fields to include in the transformation using the `$only` property: 458 | 459 | ```php 460 | final class ArticleTransformer extends ModelTransformer 461 | { 462 | protected string $dataClass = ArticleData::class; 463 | 464 | protected array $only = [ 465 | 'title', 466 | 'description', 467 | 'status', 468 | 'modified', 469 | ]; 470 | } 471 | ``` 472 | 473 | If we need to do something more complex, we could instead create a `map()` method: 474 | 475 | ```php 476 | final class ArticleTransformer extends ModelTransformer 477 | { 478 | protected string $dataClass = ArticleData::class; 479 | 480 | protected function map(Model $model): array 481 | { 482 | return [ 483 | 'title' => Str::title($model->title), 484 | 'status' => in_array($model->status, ['published', 'active']) ? 'published' : 'draft', 485 | ...$model->only('description', 'modified'), 486 | ]; 487 | } 488 | } 489 | ``` 490 | 491 | We can also pass additional parameters to our map method: 492 | 493 | ```php 494 | $transformer->transform(Article::first(), foo: 'bar'); 495 | $transformer->transformCollection(Article::all(), foo: 'bar', bar: 'baz'); 496 | ``` 497 | 498 | The `override()` and `exclude()` method can be chained to allow on-the-fly changes to the transformation: 499 | 500 | ```php 501 | $transformer 502 | ->exclude('foo', 'bar') 503 | ->override(['status' => 'preview']) 504 | ->transform(Article::first()); //transform() or transformCollection() 505 | ``` 506 | 507 | That's all good, but what about relationships: 508 | 509 | ```php 510 | final class ArticleTransformer extends ModelTransformer 511 | { 512 | protected string $dataClass = ArticleData::class; 513 | 514 | protected function map(Model $model): array 515 | { 516 | return [ 517 | 'title' => Str::title($model->title), 518 | 'status' => in_array($model->status, ['published', 'active']) ? 'published' : 'draft', 519 | ...$model->only('description', 'modified'), 520 | 'tags' => $this->includeRelated('tags', $model->tags), 521 | 'category' => $this->requireRelated('category', $model->category), 522 | ]; 523 | } 524 | } 525 | ``` 526 | 527 | The `includeRelated()` and `requireRelated()` methods will convert a `Model` using a`ModelTransformer` based on the 528 | transformation mappings in this package's config file. 529 | 530 | The `requireRelated()` method will throw a `Honeystone\DtoTools\Exceptions\RequiredRelationNotLoadedException` if the 531 | relationship has not been loaded. 532 | 533 | You can pass additional parameters to these methods, which will be passed onto their respective `ModelTransformer`'s 534 | `map()` method. 535 | 536 | Any exclusions or overrides can also be included as additional parameters: 537 | 538 | ```php 539 | final class ArticleTransformer extends ModelTransformer 540 | { 541 | protected string $dataClass = ArticleData::class; 542 | 543 | protected function map(Model $model): array 544 | { 545 | return [ 546 | //... 547 | 'category' => $this->requireRelated( 548 | 'category', 549 | $model->category, 550 | exclude: ['foo', 'bar'], 551 | override: ['status' => 'preview'], 552 | bar: 'baz', //passed onto map() method 553 | ), 554 | ]; 555 | } 556 | } 557 | ``` 558 | 559 | That's pretty much it! If you find this package useful, we'd love to hear from you. 560 | 561 | ## Changelog 562 | 563 | A list of changes can be found in the [CHANGELOG.md](CHANGELOG.md) file. 564 | 565 | ## License 566 | 567 | [MIT](LICENSE.md) © George Palmer, [Honeystone Consulting Ltd](https://honeystone.com) 568 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "honeystone/laravel-dto-tools", 3 | "description": "Comprehensive set of DTO Tools for Laravel.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "honeystone", 7 | "dto", 8 | "laravel" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "George Palmer", 13 | "email": "george@honeystone.com", 14 | "role": "Developer" 15 | } 16 | ], 17 | "homepage": "https://honeystone.com", 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/contracts": "^10.0|^11.0|^12.0", 21 | "spatie/laravel-package-tools": "^1.16.4" 22 | }, 23 | "require-dev": { 24 | "laradumps/laradumps-core": "^2.0", 25 | "larastan/larastan": "^3.0", 26 | "laravel/pint": "^1.21", 27 | "nunomaduro/collision": "^8.0", 28 | "orchestra/testbench": "^10.0", 29 | "pestphp/pest": "^3.0", 30 | "pestphp/pest-plugin-laravel": "^3.0", 31 | "phpstan/extension-installer": "^1.4", 32 | "phpstan/phpstan-deprecation-rules": "^2.0", 33 | "phpstan/phpstan-phpunit": "^2.0", 34 | "phpunit/phpunit": "^11.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Honeystone\\DtoTools\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Honeystone\\DtoTools\\Tests\\": "tests", 44 | "Workbench\\App\\": "workbench/app/", 45 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 46 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 47 | } 48 | }, 49 | "scripts": { 50 | "analyse": "vendor/bin/phpstan analyse", 51 | "test": "vendor/bin/pest", 52 | "test-coverage": "vendor/bin/pest --coverage", 53 | "post-autoload-dump": [ 54 | "@clear", 55 | "@prepare" 56 | ], 57 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 58 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 59 | "build": "@php vendor/bin/testbench workbench:build --ansi", 60 | "serve": [ 61 | "Composer\\Config::disableProcessTimeout", 62 | "@build", 63 | "@php vendor/bin/testbench serve" 64 | ], 65 | "lint": [ 66 | "@php vendor/bin/phpstan analyse" 67 | ] 68 | }, 69 | "config": { 70 | "sort-packages": true, 71 | "allow-plugins": { 72 | "phpstan/extension-installer": true, 73 | "dealerdirect/phpcodesniffer-composer-installer": true, 74 | "pestphp/pest-plugin": true 75 | } 76 | }, 77 | "extra": { 78 | "laravel": { 79 | "providers": [ 80 | "Honeystone\\DtoTools\\Providers\\DtoToolsServiceProvider" 81 | ] 82 | } 83 | }, 84 | "minimum-stability": "stable" 85 | } 86 | -------------------------------------------------------------------------------- /config/honeystone-dto-tools.php: -------------------------------------------------------------------------------- 1 | [ 7 | // App\Models\User::class => App\Data\Transformers\UserTransformer::class 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src 5 | 6 | tmpDir: build/phpstan 7 | 8 | checkModelProperties: true 9 | 10 | reportUnmatchedIgnoredErrors: false 11 | treatPhpDocTypesAsCertain: false 12 | 13 | polluteScopeWithLoopInitialAssignments: false 14 | polluteScopeWithAlwaysIterableForeach: false 15 | checkExplicitMixedMissingReturn: true 16 | checkFunctionNameCase: true 17 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests/Integration 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "phpdoc_separation": false, 5 | "not_operator_with_successor_space": false, 6 | "global_namespace_import": { 7 | "import_classes": true, 8 | "import_constants": true, 9 | "import_functions": true 10 | }, 11 | "phpdoc_align": { 12 | "align": "left", 13 | "spacing": 1 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Attributes/Patch.php: -------------------------------------------------------------------------------- 1 | $relations 14 | */ 15 | public function __construct(public array $relations) {} 16 | } 17 | -------------------------------------------------------------------------------- /src/Attributes/ToOne.php: -------------------------------------------------------------------------------- 1 | $relations 14 | */ 15 | public function __construct(public array $relations) {} 16 | } 17 | -------------------------------------------------------------------------------- /src/Casters/Contracts/CastsValues.php: -------------------------------------------------------------------------------- 1 | format !== null ? 29 | $value->format($this->format) : 30 | $value->toIso8601String(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Casters/EnumCaster.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $types; 20 | 21 | /** 22 | * @param class-string|array ...$types 23 | */ 24 | public function __construct(string|array ...$types) 25 | { 26 | $this->types = is_array($types[0] ?? null) ? $types[0] : $types; 27 | } 28 | 29 | public function cast(mixed $value): mixed 30 | { 31 | if ($value === null) { 32 | return null; 33 | } 34 | 35 | foreach ($this->types as $type) { 36 | 37 | if (!enum_exists($type) || $value instanceof $type) { 38 | continue; 39 | } 40 | 41 | /** @phpstan-ignore-next-line */ 42 | $enum = $type::tryFrom($value); 43 | 44 | if ($enum !== null) { 45 | return $enum; 46 | } 47 | } 48 | 49 | return $value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Casters/NullCaster.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $values; 20 | 21 | /** 22 | * @param string|array ...$values 23 | */ 24 | public function __construct(mixed ...$values) 25 | { 26 | $this->values = is_array($values[0] ?? null) ? $values[0] : $values; 27 | } 28 | 29 | public function cast(mixed $value): mixed 30 | { 31 | return in_array($value, $this->values, true) ? null : $value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Casters/ScalarCaster.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private array $types; 23 | 24 | /** 25 | * @param string|array ...$types 26 | */ 27 | public function __construct(string|array ...$types) 28 | { 29 | $this->types = is_array($types[0] ?? null) ? $types[0] : $types; 30 | } 31 | 32 | public function cast(mixed $value): string|int|bool|float|null 33 | { 34 | if (!is_scalar($value)) { 35 | return $value; 36 | } 37 | 38 | $lastType = null; 39 | 40 | foreach ($this->types as $type) { 41 | 42 | if (!in_array($type, ['string', 'int', 'bool', 'float', 'null'])) { 43 | throw new UnexpectedValueException( 44 | "A valid scalar type was expected, `{$type}` provided.", 45 | ); 46 | } 47 | 48 | if (gettype($value) === $type) { 49 | return $value; 50 | } 51 | 52 | $lastType = $type; 53 | } 54 | 55 | return match ($lastType) { 56 | 'string' => (string) $value, 57 | 'int' => (int) $value, 58 | 'bool' => (bool) $value, 59 | 'float' => (float) $value, 60 | default => null, 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Concerns/CreatableFromArray.php: -------------------------------------------------------------------------------- 1 | $parameters 28 | * 29 | * @return array 30 | */ 31 | final protected static function processIncoming(array $parameters): array 32 | { 33 | if (method_exists(static::class, 'transformIncoming')) { 34 | $parameters = static::transformIncoming($parameters); 35 | } 36 | 37 | return self::parseIncoming($parameters); 38 | } 39 | 40 | /** 41 | * @param array $parameters 42 | * 43 | * @return array 44 | */ 45 | private static function parseIncoming(array $parameters): array 46 | { 47 | $reflectionClass = new ReflectionClass(static::class); 48 | 49 | foreach ($reflectionClass->getProperties() as $property) { 50 | 51 | $attributes = $property->getAttributes( 52 | CastsValues::class, 53 | ReflectionAttribute::IS_INSTANCEOF, 54 | ); 55 | 56 | foreach ($attributes as $attribute) { 57 | 58 | $name = $property->getName(); 59 | 60 | if (!array_key_exists($name, $parameters)) { 61 | continue; 62 | } 63 | 64 | $parameters[$name] = $attribute->newInstance()->cast($parameters[$name]); 65 | } 66 | } 67 | 68 | return $parameters; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Concerns/CreatableFromSnake.php: -------------------------------------------------------------------------------- 1 | $parameters 15 | * 16 | * @return array 17 | */ 18 | protected static function transformIncoming(array $parameters): array 19 | { 20 | return collect($parameters)->mapWithKeys( 21 | static fn (mixed $value, string $key): array => [Str::camel($key) => $value], 22 | )->all(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Concerns/HasStorableData.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $forced = []; 22 | 23 | public function isPatching(): bool 24 | { 25 | return count((new ReflectionClass($this))->getAttributes(Patch::class)) > 0; 26 | } 27 | 28 | final public function isStorable(string $attribute): bool 29 | { 30 | if (!$this->isPatching()) { 31 | return true; 32 | } 33 | 34 | return isset($this->$attribute) || 35 | isset($this->relationships()->$attribute) || 36 | in_array($attribute, $this->getForced(), true); 37 | } 38 | 39 | final public function getForced(): array 40 | { 41 | return $this->forced; 42 | } 43 | 44 | final public function force(string ...$attributes): static 45 | { 46 | $this->forced = $attributes; 47 | 48 | return $this; 49 | } 50 | 51 | public function toStorableArray(): array 52 | { 53 | $raw = $this->toRawArray(); 54 | 55 | if ($this->isPatching()) { 56 | 57 | $raw = collect($raw) 58 | ->reject(fn (mixed $value, string $attribute): bool => !$this->isStorable($attribute)) 59 | ->toArray(); 60 | } 61 | 62 | return $this->processOutgoing($raw + $this->relationships()->toArray()); 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | private function excludeFromSerialization(): array 69 | { 70 | return ['forced', 'relationships']; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Concerns/HasStorableRelationships.php: -------------------------------------------------------------------------------- 1 | relationships === null) { 20 | 21 | $this->relationships = new Relationships( 22 | $this->getAttributeArg(ToOne::class), 23 | $this->getAttributeArg(ToMany::class), 24 | ); 25 | } 26 | 27 | return $this->relationships; 28 | } 29 | 30 | /** 31 | * @return array|null> 32 | */ 33 | public function getRelationships(): array 34 | { 35 | return $this->relationships()->toArray(); 36 | } 37 | 38 | /** 39 | * @param class-string $class 40 | * 41 | * @return array 42 | */ 43 | private function getAttributeArg(string $class): array 44 | { 45 | $attributes = (new ReflectionClass($this))->getAttributes($class); 46 | 47 | return count($attributes) > 0 ? $attributes[0]->getArguments()[0] : []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Concerns/HasTransferableData.php: -------------------------------------------------------------------------------- 1 | {$this->keyProperty ?? 'id'}; 14 | } 15 | 16 | /** 17 | * @return array 18 | */ 19 | public function getAttributes(): array 20 | { 21 | return $this->toRawArray(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Concerns/SerializesToArray.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final public function toRawArray(): array 20 | { 21 | $skip = method_exists($this, 'excludeFromSerialization') ? 22 | $this->excludeFromSerialization() : 23 | ['keyProperty']; 24 | 25 | $serialized = []; 26 | 27 | $reflectionClass = new ReflectionClass($this); 28 | 29 | foreach ($reflectionClass->getProperties() as $property) { 30 | 31 | if (!in_array($property->getName(), $skip)) { 32 | $serialized[$property->getName()] = $property->getValue($this); 33 | } 34 | } 35 | 36 | return $serialized; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | final public function toArray(): array 43 | { 44 | return $this->processOutgoing($this->toRawArray()); 45 | } 46 | 47 | /** 48 | * @param array $serialized 49 | * 50 | * @return array 51 | */ 52 | final protected function processOutgoing(array $serialized): array 53 | { 54 | if (method_exists($this, 'transformOutgoing')) { 55 | $serialized = $this->transformOutgoing($serialized); 56 | } 57 | 58 | return $this->parseOutgoing($serialized); 59 | } 60 | 61 | /** 62 | * @param array $serialized 63 | * 64 | * @return array 65 | */ 66 | private function parseOutgoing(array $serialized): array 67 | { 68 | foreach ($serialized as $name => $value) { 69 | 70 | if ($value instanceof Arrayable) { 71 | $serialized[$name] = $value->toArray(); 72 | } 73 | 74 | if (is_array($value)) { 75 | $serialized[$name] = $this->parseOutgoing($value); 76 | } 77 | } 78 | 79 | return $serialized; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Concerns/SerializesToSnake.php: -------------------------------------------------------------------------------- 1 | $data 15 | * 16 | * @return array 17 | */ 18 | protected function transformOutgoing(array $data): array 19 | { 20 | return collect($data)->mapWithKeys( 21 | static fn (mixed $value, string $key): array => [Str::snake($key) => $value], 22 | )->toArray(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Contracts/Storable.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function getForced(): array; 31 | 32 | /** 33 | * @see isPatching() 34 | */ 35 | public function force(string ...$attributes): static; 36 | 37 | /** 38 | * Manage storable relationships. 39 | */ 40 | public function relationships(): StorableRelationships; 41 | 42 | /** 43 | * Get an array of all relationships. 44 | * 45 | * @return array|null> 46 | */ 47 | public function getRelationships(): array; 48 | 49 | /** 50 | * Get an array of only storable attributes. 51 | * 52 | * @return array 53 | */ 54 | public function toStorableArray(): array; 55 | } 56 | -------------------------------------------------------------------------------- /src/Contracts/StorableRelationships.php: -------------------------------------------------------------------------------- 1 | |null 39 | */ 40 | public function getManyRelated(string $relationship): ?array; 41 | 42 | /** 43 | * Get the additions to the specified to-many relationship. 44 | * 45 | * @return array 46 | */ 47 | public function getManyAdditions(string $relationship): array; 48 | 49 | /** 50 | * Get the deletions form the specified to-many relationship. 51 | * 52 | * @return array 53 | */ 54 | public function getManyRemovals(string $relationship): array; 55 | 56 | /** 57 | * Add the given id to the specified to-many relationship. 58 | * 59 | * @param array|Transferable|null $meta 60 | * 61 | * @see removeToManyRelation() 62 | */ 63 | public function addToManyRelation( 64 | string $relationship, 65 | int|string|Transferable $id, 66 | array|Transferable|null $meta = null, 67 | ): void; 68 | 69 | /** 70 | * Remove the given id from the specified to many relationship. 71 | * 72 | * @see addToManyRelation() 73 | */ 74 | public function removeToManyRelation(string $relationship, int|string|Transferable $id): void; 75 | 76 | /** 77 | * Replace the specified to-many relationship overriding any existing 78 | * additions or deletions. 79 | * 80 | * @param array>|null, 83 | * }>|null $ids 84 | * @param array>|Transferable $meta 85 | */ 86 | public function replaceToMany( 87 | string $relationship, 88 | ?array $ids = null, 89 | array|Transferable $meta = [], 90 | ): void; 91 | 92 | /** 93 | * Reset the specified to-many relationship. 94 | */ 95 | public function resetToMany(string $relationship): void; 96 | 97 | /** 98 | * @return array> 99 | */ 100 | public function getMetaData(string $relationship): array; 101 | 102 | /** 103 | * @param array>|Transferable $meta 104 | */ 105 | public function setMetaData(string $relationship, array|Transferable $meta = []): void; 106 | 107 | /** 108 | * Get an array of all relationships. 109 | * 110 | * @return array|null> 111 | */ 112 | public function toArray(): array; 113 | } 114 | -------------------------------------------------------------------------------- /src/Contracts/Transferable.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface Transferable extends Arrayable 13 | { 14 | public static function make(mixed ...$parameters): static; 15 | 16 | public function getKey(): int|string; 17 | 18 | /** 19 | * Get all attributes. 20 | * 21 | * @return array 22 | */ 23 | public function getAttributes(): array; 24 | } 25 | -------------------------------------------------------------------------------- /src/Exceptions/RelationNotFoundException.php: -------------------------------------------------------------------------------- 1 | name('honeystone-dto-tools') 21 | ->setBasePath(dirname(__DIR__)) 22 | ->hasConfigFile('honeystone-dto-tools'); 23 | } 24 | 25 | public function packageRegistered(): void 26 | { 27 | $this->app->bind( 28 | MakesTransformers::class, 29 | static fn (): TransformerFactory => new TransformerFactory(config('honeystone-dto-tools.transformers')), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Relationships.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $one = []; 27 | 28 | /** 29 | * @var array|null> 30 | */ 31 | private array $many = []; 32 | 33 | /** 34 | * @var array> 35 | */ 36 | private array $additions = []; 37 | 38 | /** 39 | * @var array> 40 | */ 41 | private array $deletions = []; 42 | 43 | /** 44 | * @var array>> 45 | */ 46 | private array $meta = []; 47 | 48 | /** 49 | * @var array> 50 | */ 51 | private array $toOne; 52 | 53 | /** 54 | * @var array> 55 | */ 56 | private array $toMany; 57 | 58 | /** 59 | * @param array $toOne 60 | * @param array $toMany 61 | */ 62 | public function __construct(array $toOne, array $toMany) 63 | { 64 | $this->unpackRelationships($toOne, $toMany); 65 | } 66 | 67 | public function __get(string $name): mixed 68 | { 69 | if ($this->isToOneRelationship($name)) { 70 | return $this->getOneRelated($name); 71 | } 72 | 73 | return $this->getManyRelated($name); 74 | } 75 | 76 | public function __set(string $name, mixed $value): void 77 | { 78 | if ($this->isToOneRelationship($name)) { 79 | $this->setOneRelated($name, $value); 80 | 81 | return; 82 | } 83 | 84 | $this->replaceToMany($name, $value); 85 | } 86 | 87 | public function __isset(string $name): bool 88 | { 89 | return $this->hasToOne($name) || $this->hasToMany($name); 90 | } 91 | 92 | public function hasToOne(string $relationship): bool 93 | { 94 | return array_key_exists($relationship, $this->one); 95 | } 96 | 97 | public function getOneRelated(string $relationship): int|string|null 98 | { 99 | $this->checkToOneRelationshipExists($relationship); 100 | 101 | return $this->one[$relationship] ?? null; 102 | } 103 | 104 | public function hasToMany(string $relationship): bool 105 | { 106 | return array_key_exists($relationship, $this->many); 107 | } 108 | 109 | public function getManyRelated(string $relationship): ?array 110 | { 111 | $this->checkToManyRelationshipExists($relationship); 112 | 113 | if (!array_key_exists($relationship, $this->many) || $this->many[$relationship] === null) { 114 | return in_array('null', $this->getRelationshipTypes($relationship), true) ? null : []; 115 | } 116 | 117 | return (new Collection([ 118 | ...$this->many[$relationship], 119 | ...$this->getManyAdditions($relationship), 120 | ])) 121 | ->diff($this->getManyRemovals($relationship)) 122 | ->values() 123 | ->toArray(); 124 | } 125 | 126 | public function getManyAdditions(string $relationship): array 127 | { 128 | $this->checkToManyRelationshipExists($relationship); 129 | 130 | return $this->additions[$relationship] ?? []; 131 | } 132 | 133 | public function getManyRemovals(string $relationship): array 134 | { 135 | $this->checkToManyRelationshipExists($relationship); 136 | 137 | return $this->deletions[$relationship] ?? []; 138 | } 139 | 140 | public function setOneRelated(string $relationship, int|string|Transferable|null $id): void 141 | { 142 | $id = $this->transferableToId($id); 143 | 144 | $this->checkToOneRelationship($relationship, $id); 145 | 146 | $this->one[$relationship] = $id; 147 | } 148 | 149 | public function unsetOneRelated(string $relationship): void 150 | { 151 | $this->checkToOneRelationshipExists($relationship); 152 | 153 | unset($this->one[$relationship]); 154 | } 155 | 156 | public function addToManyRelation( 157 | string $relationship, 158 | int|string|Transferable $id, 159 | array|Transferable|null $meta = null, 160 | ): void { 161 | 162 | $id = $this->transferableToId($id); 163 | $meta = $this->transferableToMeta($meta); 164 | 165 | $this->checkToManyRelationship($relationship, $id); 166 | 167 | if (!array_key_exists($relationship, $this->additions)) { 168 | $this->additions[$relationship] = []; 169 | } 170 | 171 | if (!array_key_exists($relationship, $this->meta)) { 172 | $this->meta[$relationship] = []; 173 | } 174 | 175 | $this->additions[$relationship][] = $id; 176 | 177 | if ($meta !== null) { 178 | $this->meta[$relationship][$id] = $meta; 179 | } 180 | 181 | $this->removeFromDeletions($relationship, $id); 182 | } 183 | 184 | public function removeToManyRelation(string $relationship, int|string|Transferable $id): void 185 | { 186 | $id = $this->transferableToId($id); 187 | 188 | $this->checkToManyRelationship($relationship, $id); 189 | 190 | if (!array_key_exists($relationship, $this->deletions)) { 191 | $this->deletions[$relationship] = []; 192 | } 193 | 194 | $this->deletions[$relationship][] = $id; 195 | 196 | $this->removeFromAdditions($relationship, $id); 197 | } 198 | 199 | public function replaceToMany( 200 | string $relationship, 201 | ?array $ids = null, 202 | array|Transferable|null $meta = null, 203 | ): void { 204 | 205 | $this->checkToManyRelationshipExists($relationship); 206 | 207 | $this->meta[$relationship] = []; 208 | 209 | if ($ids === null) { 210 | $this->replaceToManyWithNull($relationship); 211 | 212 | } elseif (count($ids) === 0) { 213 | $this->replaceToManyWithEmpty($relationship); 214 | 215 | } else { 216 | $finalIds = []; 217 | 218 | foreach ($ids as $id) { 219 | [$checkId, $meta] = $this->extractMetaDataFromId($id, $meta ?? []); 220 | 221 | $this->checkRelationIdType($relationship, $checkId); 222 | 223 | $finalIds[] = $checkId; 224 | } 225 | 226 | $this->many[$relationship] = $finalIds; 227 | } 228 | 229 | $this->meta[$relationship] = $meta ?? []; 230 | 231 | unset($this->additions[$relationship], $this->deletions[$relationship]); 232 | } 233 | 234 | public function resetToMany(string $relationship): void 235 | { 236 | $this->checkToManyRelationshipExists($relationship); 237 | 238 | if ( 239 | !$this->isRelationNullable($relationship) && 240 | !$this->canRelationBeEmpty($relationship) 241 | ) { 242 | throw new RelationValidationException("The {$relationship} relationship must be set."); 243 | } 244 | 245 | unset( 246 | $this->many[$relationship], 247 | $this->additions[$relationship], 248 | $this->deletions[$relationship], 249 | $this->meta[$relationship] 250 | ); 251 | } 252 | 253 | public function getMetaData(string $relationship): array 254 | { 255 | return $this->meta[$relationship] ?? []; 256 | } 257 | 258 | public function setMetaData(string $relationship, array|Transferable $meta = []): void 259 | { 260 | $this->meta[$relationship] = $this->transferableToMeta($meta); 261 | } 262 | 263 | public function toArray(): array 264 | { 265 | [$one, $many] = (new Collection([$this->toOne, $this->toMany])) 266 | ->map( 267 | fn (array $relationships): array => (new Collection($relationships)) 268 | ->map(function (array $types, string $relationship): int|string|array|null { 269 | 270 | if ($this->isToOneRelationship($relationship)) { 271 | return $this->getOneRelated($relationship); 272 | } 273 | 274 | return $this->getManyRelated($relationship); 275 | })->toArray(), 276 | )->toArray(); 277 | 278 | return array_merge($one, $many); 279 | } 280 | 281 | /** 282 | * Unpack shorthand relationships to allowed types. 283 | * 284 | * @param array $toOne 285 | * @param array $toMany 286 | */ 287 | private function unpackRelationships(array $toOne, array $toMany): void 288 | { 289 | [$this->toOne, $this->toMany] = (new Collection([$toOne, $toMany])) 290 | ->map( 291 | static fn (array $relationships): array => (new Collection($relationships)) 292 | ->mapWithKeys(static function (string $value, int|string $key): array { 293 | 294 | if (is_string($key)) { 295 | return [$key => explode('|', $value)]; 296 | } 297 | 298 | return [$value => ['int', 'string', 'empty', 'null']]; 299 | })->toArray(), 300 | )->toArray(); 301 | } 302 | 303 | /** 304 | * Check the relationship being set or removed. 305 | */ 306 | private function checkToOneRelationship(string $relationship, int|string|null $id): void 307 | { 308 | $this->checkToOneRelationshipExists($relationship); 309 | $this->checkRelationIdType($relationship, $id); 310 | } 311 | 312 | /** 313 | * Check the relationship being added or removed. 314 | */ 315 | private function checkToManyRelationship(string $relationship, int|string $id): void 316 | { 317 | $this->checkToManyRelationshipExists($relationship); 318 | $this->checkRelationIdType($relationship, $id); 319 | } 320 | 321 | /** 322 | * Check the one relationship exits on this DTO. 323 | */ 324 | private function checkToOneRelationshipExists(string $relationship): void 325 | { 326 | if (!array_key_exists($relationship, $this->toOne)) { 327 | throw new RelationNotFoundException('To-one relationship `'.$relationship.'` not found.'); 328 | } 329 | } 330 | 331 | /** 332 | * Check the many relationship exits on this DTO. 333 | */ 334 | private function checkToManyRelationshipExists(string $relationship): void 335 | { 336 | if (!array_key_exists($relationship, $this->toMany)) { 337 | throw new RelationNotFoundException('To-many relationship `'.$relationship.'` not found.'); 338 | } 339 | } 340 | 341 | private function isRelationNullable(string $relationship): bool 342 | { 343 | $expectedTypes = $this->getRelationshipTypes($relationship); 344 | 345 | return in_array('null', $expectedTypes, true); 346 | } 347 | 348 | private function canRelationBeEmpty(string $relationship): bool 349 | { 350 | $expectedTypes = $this->getRelationshipTypes($relationship); 351 | 352 | return in_array('empty', $expectedTypes, true); 353 | } 354 | 355 | /** 356 | * Check the specified relation's id type. 357 | */ 358 | private function checkRelationIdType(string $relationship, int|string|null $id): void 359 | { 360 | $expectedTypes = $this->getRelationshipTypes($relationship); 361 | $givenType = strtolower(gettype($id)); 362 | 363 | if (!in_array($givenType, $expectedTypes, true)) { 364 | throw new RelationValidationException( 365 | 'The related id should be of the type '. 366 | implode('|', $expectedTypes).", instead the id was `{$id}` ({$givenType}).", 367 | ); 368 | } 369 | } 370 | 371 | /** 372 | * Get the specified relationship's types. 373 | * 374 | * @return array 375 | */ 376 | private function getRelationshipTypes(string $relationship): array 377 | { 378 | $types = $this->isToOneRelationship($relationship) ? 379 | $this->toOne[$relationship] : 380 | $this->toMany[$relationship]; 381 | 382 | return (new Collection($types))->map( 383 | static function (string $type): string { 384 | $type = str_replace('[]', '', $type); 385 | 386 | if ($type === 'int') { 387 | return 'integer'; 388 | } 389 | 390 | return $type; 391 | }, 392 | )->toArray(); 393 | } 394 | 395 | /** 396 | * Check if the specified relationship is a to-one relationship. 397 | */ 398 | private function isToOneRelationship(string $relationship): bool 399 | { 400 | return array_key_exists($relationship, $this->toOne); 401 | } 402 | 403 | /** 404 | * Remove an id from the specified relationship's additions. 405 | */ 406 | private function removeFromAdditions(string $relationship, int|string $id): void 407 | { 408 | $additions = $this->getManyAdditions($relationship); 409 | 410 | if (in_array($id, $additions, true)) { 411 | $this->additions[$relationship] = Arr::except($additions, $id); 412 | } 413 | } 414 | 415 | /** 416 | * Remove an id from the specified relationship's deletions. 417 | */ 418 | private function removeFromDeletions(string $relationship, int|string $id): void 419 | { 420 | $deletions = $this->getManyRemovals($relationship); 421 | 422 | if (in_array($id, $deletions, true)) { 423 | $this->deletions[$relationship] = Arr::except($deletions, $id); 424 | } 425 | } 426 | 427 | private function replaceToManyWithNull(string $relationship): void 428 | { 429 | if (!$this->isRelationNullable($relationship)) { 430 | throw new RelationValidationException("The {$relationship} relationship must not be null."); 431 | } 432 | 433 | $this->many[$relationship] = null; 434 | } 435 | 436 | private function replaceToManyWithEmpty(string $relationship): void 437 | { 438 | if (!$this->canRelationBeEmpty($relationship)) { 439 | 440 | throw new RelationValidationException("The {$relationship} relationship must not be empty."); 441 | } 442 | 443 | $this->many[$relationship] = []; 444 | } 445 | 446 | /** 447 | * @param int|string|Transferable|array{ 448 | * id: int|string|Transferable, 449 | * pivot?: array>|null, 450 | * }|null $id 451 | * 452 | * @param array> $existingPivot 453 | * 454 | * @return array{0: int|string, 1: array>} 455 | */ 456 | private function extractMetaDataFromId(int|string|Transferable|array|null $id, array $existingPivot): array 457 | { 458 | $realId = $id; 459 | 460 | if (is_array($id) && array_key_exists('id', $id)) { 461 | $realId = $id['id']; 462 | $existingPivot[$realId] = $id['pivot'] ?? []; 463 | } 464 | 465 | return [ 466 | $this->transferableToId($realId), 467 | $this->transferableToMeta($existingPivot), 468 | ]; 469 | } 470 | 471 | private function transferableToId(int|string|Transferable|null $id): int|string|null 472 | { 473 | if ($id instanceof Transferable) { 474 | return $id->getKey(); 475 | } 476 | 477 | return $id; 478 | } 479 | 480 | /** 481 | * @param array>|Transferable|null $meta 482 | * 483 | * @return array>|null 484 | */ 485 | private function transferableToMeta(array|Transferable|null $meta): ?array 486 | { 487 | if ($meta instanceof Transferable) { 488 | return $meta->toArray(); 489 | } 490 | 491 | return $meta; 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /src/Transformers/Concerns/ManipulatesData.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $excluded = []; 22 | 23 | /** 24 | * @var array 25 | */ 26 | private array $overridden = []; 27 | 28 | public function exclude(string ...$attributes): self 29 | { 30 | $this->excluded = $attributes; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @param array $attributes 37 | */ 38 | public function override(array $attributes): self 39 | { 40 | $this->overridden = $attributes; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @param array $mapped 47 | * 48 | * @return array 49 | */ 50 | final protected function processData(array $mapped): array 51 | { 52 | $processing = $mapped; 53 | 54 | foreach ($this->excluded as $attribute) { 55 | if (array_key_exists($attribute, $processing)) { 56 | unset($processing[$attribute]); 57 | } 58 | } 59 | 60 | foreach ($this->overridden as $attribute => $value) { 61 | $processing[$attribute] = $value; 62 | } 63 | 64 | $this->excluded = []; 65 | $this->overridden = []; 66 | 67 | return $processing; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Transformers/Concerns/TransformsRelations.php: -------------------------------------------------------------------------------- 1 | |Transferable|null 26 | */ 27 | final protected function requireRelated( 28 | string $relation, 29 | Model $model, 30 | mixed ...$parameters, 31 | ): Collection|Transferable|null { 32 | 33 | if (!$model->relationLoaded($relation)) { 34 | throw new RequiredRelationNotLoadedException($this->getDataClass(), $model::class, $relation); 35 | } 36 | 37 | return $this->includeRelated($relation, $model, ...$parameters); 38 | } 39 | 40 | /** 41 | * @param TModel $model 42 | * 43 | * @return Collection|Transferable|null 44 | */ 45 | final protected function includeRelated( 46 | string $relation, 47 | Model $model, 48 | mixed ...$parameters, 49 | ): Collection|Transferable|null { 50 | 51 | if (is_array($parameters[0] ?? null)) { 52 | $parameters = $parameters[0]; 53 | } 54 | 55 | if (!$model->relationLoaded($relation)) { 56 | return null; 57 | } 58 | 59 | $related = $model->$relation; 60 | 61 | if ($related === null) { 62 | return null; 63 | } 64 | 65 | if (array_key_exists('callback', $parameters)) { 66 | 67 | $parameters['callback']($related); 68 | 69 | unset($parameters['callback']); 70 | } 71 | 72 | if ($related instanceof Model) { 73 | return $this->transformRelatedModel($related, $parameters); 74 | } 75 | 76 | return $this->transformRelatedCollection($related, $parameters); 77 | } 78 | 79 | abstract protected function getDataClass(): string; 80 | 81 | abstract protected function getTransformerFactory(): MakesTransformers; 82 | 83 | /** 84 | * @param array $parameters 85 | */ 86 | final protected function transformRelatedModel(Model $model, array $parameters = []): Transferable 87 | { 88 | [$exclude, $override, $parameters] = $this->extractManipulations($parameters); 89 | 90 | return $this->getTransformerFactory() 91 | ->makeForModel($model) 92 | ->exclude(...$exclude) 93 | ->override($override) 94 | ->transform($model, ...$parameters); 95 | } 96 | 97 | /** 98 | * @param EloquentCollection $models 99 | * @param array $parameters 100 | * 101 | * @return Collection 102 | */ 103 | final protected function transformRelatedCollection(EloquentCollection $models, array $parameters = []): Collection 104 | { 105 | if ($models->isEmpty()) { 106 | return new Collection; 107 | } 108 | 109 | [$exclude, $override, $parameters] = $this->extractManipulations($parameters); 110 | 111 | return $this->getTransformerFactory() 112 | ->makeForCollection($models) 113 | ->exclude(...$exclude) 114 | ->override($override) 115 | ->transformCollection($models, ...$parameters); 116 | } 117 | 118 | /** 119 | * @param array $parameters 120 | * 121 | * @return array{0: array, 1: array, 2: array} 122 | */ 123 | private function extractManipulations(array $parameters = []): array 124 | { 125 | $exclude = []; 126 | $override = []; 127 | 128 | if (array_key_exists('exclude', $parameters)) { 129 | $exclude = $parameters['exclude']; 130 | unset($parameters['exclude']); 131 | } 132 | 133 | if (array_key_exists('override', $parameters)) { 134 | $override = $parameters['override']; 135 | unset($parameters['override']); 136 | } 137 | 138 | return [$exclude, $override, $parameters]; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Transformers/Contracts/MakesTransformers.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function makeForModel(Model $model): TransformsModels; 21 | 22 | /** 23 | * @template TModel of Model 24 | * 25 | * @param Collection $models 26 | * 27 | * @return TransformsCollections 28 | */ 29 | public function makeForCollection(Collection $models): TransformsCollections; 30 | } 31 | -------------------------------------------------------------------------------- /src/Transformers/Contracts/ManipulatesSchema.php: -------------------------------------------------------------------------------- 1 | $attributes 16 | * 17 | * @return $this 18 | */ 19 | public function override(array $attributes): self; 20 | } 21 | -------------------------------------------------------------------------------- /src/Transformers/Contracts/TransformsCollections.php: -------------------------------------------------------------------------------- 1 | $models 23 | * 24 | * @return Collection 25 | */ 26 | public function transformCollection(EloquentCollection $models, mixed ...$parameters): Collection; 27 | } 28 | -------------------------------------------------------------------------------- /src/Transformers/Contracts/TransformsModels.php: -------------------------------------------------------------------------------- 1 | 26 | * @implements TransformsCollections 27 | */ 28 | abstract class ModelTransformer implements TransformsCollections, TransformsModels 29 | { 30 | /** 31 | * @use ManipulatesData 32 | */ 33 | use ManipulatesData; 34 | 35 | /** 36 | * @use TransformsRelations 37 | */ 38 | use TransformsRelations; 39 | 40 | /** 41 | * @var class-string 42 | */ 43 | protected string $dataClass; 44 | 45 | protected string $dataKeyName = 'id'; 46 | 47 | public function __construct(private readonly MakesTransformers $transformerFactory) {} 48 | 49 | /** 50 | * @param TModel $model 51 | * 52 | * @return TData 53 | */ 54 | public function transform(Model $model, mixed ...$parameters): Transferable 55 | { 56 | if (is_array($parameters[0] ?? null)) { 57 | $parameters = $parameters[0]; 58 | } 59 | 60 | return $this->makeData( 61 | $this->processData([ 62 | $this->dataKeyName => $this->getModelKey($model), 63 | ...$this->mapModel($model, $parameters), 64 | ]), 65 | ); 66 | } 67 | 68 | /** 69 | * @param EloquentCollection $models 70 | * 71 | * @return Collection 72 | */ 73 | public function transformCollection(EloquentCollection $models, mixed ...$parameters): Collection 74 | { 75 | /** @phpstan-ignore-next-line */ 76 | return $models 77 | ->map(fn (Model $model): Transferable => $this->transform($model, ...$parameters)) 78 | ->values() 79 | ->collect(); 80 | } 81 | 82 | protected function getModelKey(Model $model): int|string 83 | { 84 | return $model->getKey(); 85 | } 86 | 87 | protected function getDataClass(): string 88 | { 89 | return $this->dataClass; 90 | } 91 | 92 | /** 93 | * @param TModel $model 94 | * @param array $parameters 95 | * 96 | * @return array 97 | */ 98 | final protected function mapModel(Model $model, array $parameters): array 99 | { 100 | if (method_exists($this, 'map')) { 101 | return $this->map($model, ...$parameters); 102 | } 103 | 104 | if (property_exists($this, 'only')) { 105 | return $model->only($this->only); 106 | } 107 | 108 | return $model->toArray(); 109 | } 110 | 111 | /** 112 | * @param array $attributes 113 | * 114 | * @return TData 115 | */ 116 | final protected function makeData(array $attributes): Transferable 117 | { 118 | return $this->dataClass::make(...$attributes); 119 | } 120 | 121 | final protected function getTransformerFactory(): MakesTransformers 122 | { 123 | return $this->transformerFactory; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Transformers/TransformerFactory.php: -------------------------------------------------------------------------------- 1 | , 23 | * class-string|TransformsCollections> 24 | * > $map 25 | */ 26 | public function __construct(private array $map) {} 27 | 28 | public function makeForModel(Model $model): TransformsModels 29 | { 30 | return app($this->getTransformerClass($model::class)); 31 | } 32 | 33 | public function makeForCollection(Collection $models): TransformsCollections 34 | { 35 | if ($models->isEmpty()) { 36 | throw new RuntimeException('Unable to determine mapping model from empty collection.'); 37 | } 38 | 39 | return app($this->getTransformerClass($models->first()::class)); 40 | } 41 | 42 | /** 43 | * @template TModel of Model 44 | * 45 | * @param class-string $modelClass 46 | * 47 | * @return class-string|TransformsCollections> 48 | */ 49 | private function getTransformerClass(string $modelClass): string 50 | { 51 | if (!array_key_exists($modelClass, $this->map)) { 52 | throw new RuntimeException( 53 | "The {$modelClass} model must have a configured transformer before this factory can make it.", 54 | ); 55 | } 56 | 57 | return $this->map[$modelClass]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | providers: 2 | # - Workbench\App\Providers\WorkbenchServiceProvider 3 | 4 | migrations: 5 | - workbench/database/migrations 6 | 7 | seeders: 8 | - Workbench\Database\Seeders\DatabaseSeeder 9 | 10 | workbench: 11 | start: '/' 12 | install: true 13 | health: false 14 | discovers: 15 | web: true 16 | api: false 17 | commands: false 18 | components: false 19 | views: false 20 | build: [] 21 | assets: [] 22 | sync: [] 23 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Data/BarData.php: -------------------------------------------------------------------------------- 1 | |null $baz 21 | */ 22 | public function __construct( 23 | #[ScalarCaster('int')] 24 | public int $id, 25 | 26 | public string $name, 27 | public string $description, 28 | 29 | #[ScalarCaster('bool')] 30 | public bool $featured, 31 | 32 | #[EnumCaster(State::class)] 33 | public State $state, 34 | 35 | #[DateTimeCaster] 36 | public string $modified, 37 | 38 | public ?BarData $bar = null, 39 | public ?Collection $baz = null, 40 | ) {} 41 | } 42 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Enums/State.php: -------------------------------------------------------------------------------- 1 | hasOne(Bar::class); 18 | } 19 | 20 | public function baz(): HasMany 21 | { 22 | return $this->hasMany(Bar::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Transformers/BarTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class BarTransformer extends ModelTransformer 15 | { 16 | protected string $dataClass = BarData::class; 17 | } 18 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Transformers/BazTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class BazTransformer extends ModelTransformer 15 | { 16 | protected string $dataClass = BazData::class; 17 | } 18 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Transformers/MapMethodTransformer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class MapMethodTransformer extends ModelTransformer 16 | { 17 | protected string $dataClass = FooData::class; 18 | 19 | /** 20 | * @param array $extra 21 | * 22 | * @return array 23 | */ 24 | protected function map(Model $model, array $extra = []): array 25 | { 26 | return $extra + $model->only( 27 | 'name', 28 | 'description', 29 | 'featured', 30 | 'state', 31 | 'modified', 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Transformers/NoMapTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class NoMapTransformer extends ModelTransformer 15 | { 16 | protected string $dataClass = FooData::class; 17 | } 18 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Transformers/OnlyPropertyTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class OnlyPropertyTransformer extends ModelTransformer 15 | { 16 | protected string $dataClass = FooData::class; 17 | 18 | /** 19 | * @var array 20 | */ 21 | protected array $only = [ 22 | 'name', 23 | 'description', 24 | 'featured', 25 | 'state', 26 | 'modified', 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/Fixtures/Transformers/RelationsTransformer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class RelationsTransformer extends ModelTransformer 16 | { 17 | protected string $dataClass = FooData::class; 18 | 19 | /** 20 | * @param array $barExtra 21 | * @param array $bazExtra 22 | * 23 | * @return array 24 | */ 25 | protected function map(Model $model, array $barExtra = [], array $bazExtra = []): array 26 | { 27 | return [ 28 | 'bar' => $this->requireRelated('bar', $model, $barExtra), 29 | 'baz' => $this->includeRelated('baz', $model, ...$bazExtra), 30 | ] + $model->only( 31 | 'name', 32 | 'description', 33 | 'featured', 34 | 'state', 35 | 'modified', 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Integration/Transformers/Concerns/ModelTransformerTest.php: -------------------------------------------------------------------------------- 1 | '1', 29 | 'name' => 'foo', 30 | 'description' => 'bar', 31 | 'featured' => 1, 32 | 'state' => 'draft', 33 | 'modified' => $modified, 34 | 'foobar' => 'baz', 35 | ]); 36 | 37 | $factory = new TransformerFactory([Foo::class => MapMethodTransformer::class]); 38 | 39 | $data = $factory->makeForModel($model)->transform($model); 40 | 41 | expect($data)->toBeInstanceOf(FooData::class) 42 | ->and($data->toArray())->tobe([ 43 | 'id' => 1, 44 | 'name' => 'foo', 45 | 'description' => 'bar', 46 | 'featured' => true, 47 | 'state' => State::DRAFT, 48 | 'modified' => $modified->toIso8601String(), 49 | 'bar' => null, 50 | 'baz' => null, 51 | ]); 52 | }); 53 | 54 | it('transforms using only property', function (): void { 55 | 56 | $modified = Carbon::now(); 57 | 58 | $model = new Foo([ 59 | 'id' => '1', 60 | 'name' => 'foo', 61 | 'description' => 'bar', 62 | 'featured' => 1, 63 | 'state' => 'draft', 64 | 'modified' => $modified, 65 | 'foobar' => 'baz', 66 | ]); 67 | 68 | $factory = new TransformerFactory([Foo::class => OnlyPropertyTransformer::class]); 69 | 70 | $data = $factory->makeForModel($model)->transform($model); 71 | 72 | expect($data)->toBeInstanceOf(FooData::class) 73 | ->and($data->toArray())->tobe([ 74 | 'id' => 1, 75 | 'name' => 'foo', 76 | 'description' => 'bar', 77 | 'featured' => true, 78 | 'state' => State::DRAFT, 79 | 'modified' => $modified->toIso8601String(), 80 | 'bar' => null, 81 | 'baz' => null, 82 | ]); 83 | }); 84 | 85 | it('fails to transform without any map when there are unknown attributes', function (): void { 86 | 87 | $modified = Carbon::now(); 88 | 89 | $model = new Foo([ 90 | 'id' => '1', 91 | 'name' => 'foo', 92 | 'description' => 'bar', 93 | 'featured' => 1, 94 | 'state' => 'draft', 95 | 'modified' => $modified, 96 | 'foobar' => 'baz', 97 | ]); 98 | 99 | $factory = new TransformerFactory([Foo::class => NoMapTransformer::class]); 100 | 101 | $factory->makeForModel($model)->transform($model); 102 | 103 | })->throws(Error::class); 104 | 105 | it('transforms a collection', function (): void { 106 | 107 | $modified = Carbon::now(); 108 | 109 | $collection = new EloquentCollection([ 110 | new Foo([ 111 | 'id' => '1', 112 | 'name' => 'foo', 113 | 'description' => 'bar', 114 | 'featured' => 1, 115 | 'state' => 'draft', 116 | 'modified' => $modified, 117 | 'foobar' => 'baz', 118 | ]), 119 | new Foo([ 120 | 'id' => '2', 121 | 'name' => 'foo', 122 | 'description' => 'bar', 123 | 'featured' => 1, 124 | 'state' => 'published', 125 | 'modified' => $modified, 126 | 'foobar' => 'baz', 127 | ]), 128 | ]); 129 | 130 | $factory = new TransformerFactory([Foo::class => MapMethodTransformer::class]); 131 | 132 | $data = $factory->makeForCollection($collection)->transformCollection($collection); 133 | 134 | expect($data)->toBeInstanceOf(Collection::class) 135 | ->and($data->first()->toArray())->tobe([ 136 | 'id' => 1, 137 | 'name' => 'foo', 138 | 'description' => 'bar', 139 | 'featured' => true, 140 | 'state' => State::DRAFT, 141 | 'modified' => $modified->toIso8601String(), 142 | 'bar' => null, 143 | 'baz' => null, 144 | ]) 145 | ->and($data->last()->toArray())->tobe([ 146 | 'id' => 2, 147 | 'name' => 'foo', 148 | 'description' => 'bar', 149 | 'featured' => true, 150 | 'state' => State::PUBLISHED, 151 | 'modified' => $modified->toIso8601String(), 152 | 'bar' => null, 153 | 'baz' => null, 154 | ]); 155 | }); 156 | 157 | it('transforms with exclusions', function (): void { 158 | 159 | $modified = Carbon::now(); 160 | 161 | $model = new Foo([ 162 | 'id' => '1', 163 | 'name' => 'foo', 164 | 'description' => 'bar', 165 | 'featured' => 1, 166 | 'state' => 'draft', 167 | 'modified' => $modified, 168 | 'foobar' => 'baz', 169 | ]); 170 | 171 | $factory = new TransformerFactory([Foo::class => NoMapTransformer::class]); 172 | 173 | $data = $factory->makeForModel($model)->exclude('foobar')->transform($model); 174 | 175 | expect($data)->toBeInstanceOf(FooData::class) 176 | ->and($data->toArray())->tobe([ 177 | 'id' => 1, 178 | 'name' => 'foo', 179 | 'description' => 'bar', 180 | 'featured' => true, 181 | 'state' => State::DRAFT, 182 | 'modified' => $modified->toIso8601String(), 183 | 'bar' => null, 184 | 'baz' => null, 185 | ]); 186 | }); 187 | 188 | it('transforms with overrides', function (): void { 189 | 190 | $modified = Carbon::now(); 191 | 192 | $model = new Foo([ 193 | 'id' => '1', 194 | 'name' => 'foo', 195 | 'description' => 'bar', 196 | 'featured' => 1, 197 | 'state' => 'draft', 198 | 'modified' => $modified, 199 | ]); 200 | 201 | $factory = new TransformerFactory([Foo::class => NoMapTransformer::class]); 202 | 203 | $data = $factory->makeForModel($model)->override(['name' => 'foobar'])->transform($model); 204 | 205 | expect($data)->toBeInstanceOf(FooData::class) 206 | ->and($data->toArray())->tobe([ 207 | 'id' => 1, 208 | 'name' => 'foobar', 209 | 'description' => 'bar', 210 | 'featured' => true, 211 | 'state' => State::DRAFT, 212 | 'modified' => $modified->toIso8601String(), 213 | 'bar' => null, 214 | 'baz' => null, 215 | ]); 216 | }); 217 | 218 | it('transforms with parameters', function (): void { 219 | 220 | $modified = Carbon::now(); 221 | 222 | $model = new Foo([ 223 | 'id' => '1', 224 | 'name' => 'foo', 225 | 'description' => 'bar', 226 | 'featured' => 1, 227 | 'state' => 'draft', 228 | ]); 229 | 230 | $factory = new TransformerFactory([Foo::class => MapMethodTransformer::class]); 231 | 232 | $data = $factory->makeForModel($model)->transform($model, extra: ['modified' => $modified]); 233 | 234 | expect($data)->toBeInstanceOf(FooData::class) 235 | ->and($data->toArray())->tobe([ 236 | 'id' => 1, 237 | 'name' => 'foo', 238 | 'description' => 'bar', 239 | 'featured' => true, 240 | 'state' => State::DRAFT, 241 | 'modified' => $modified->toIso8601String(), 242 | 'bar' => null, 243 | 'baz' => null, 244 | ]); 245 | }); 246 | 247 | it('transforms with relations, inlcluded not loaded', function (): void { 248 | 249 | app()->config->set('honeystone-dto-tools.transformers', [ 250 | Bar::class => NoMapTransformer::class, 251 | Baz::class => NoMapTransformer::class, 252 | ]); 253 | 254 | $modified = Carbon::now(); 255 | 256 | $model = $this->partialMock(Foo::class, function (MockInterface $mock): void { 257 | $mock->shouldReceive('relationLoaded')->with('bar')->twice()->andReturn(true); 258 | $mock->shouldReceive('relationLoaded')->with('baz')->once()->andReturn(false); 259 | }); 260 | 261 | $model->fill([ 262 | 'id' => '1', 263 | 'name' => 'foo', 264 | 'description' => 'bar', 265 | 'featured' => 1, 266 | 'state' => 'draft', 267 | 'modified' => $modified, 268 | 269 | // relations 270 | 'bar' => null, 271 | ]); 272 | 273 | $factory = new TransformerFactory([$model::class => RelationsTransformer::class]); 274 | 275 | $data = $factory->makeForModel($model)->transform($model); 276 | 277 | expect($data)->toBeInstanceOf(FooData::class) 278 | ->and($data->toArray())->tobe([ 279 | 'id' => 1, 280 | 'name' => 'foo', 281 | 'description' => 'bar', 282 | 'featured' => true, 283 | 'state' => State::DRAFT, 284 | 'modified' => $modified->toIso8601String(), 285 | 286 | // relations 287 | 'bar' => null, 288 | 'baz' => null, 289 | ]); 290 | }); 291 | 292 | it('transforms with relations, required not loaded', function (): void { 293 | 294 | $model = $this->partialMock(Foo::class, function (MockInterface $mock): void { 295 | $mock->shouldReceive('relationLoaded')->with('bar')->once()->andReturn(false); 296 | }); 297 | 298 | $model->fill(['id' => '1']); 299 | 300 | $factory = new TransformerFactory([$model::class => RelationsTransformer::class]); 301 | 302 | $factory->makeForModel($model)->transform($model); 303 | 304 | })->throws(RequiredRelationNotLoadedException::class); 305 | 306 | it('transforms with relations, loaded', function (): void { 307 | 308 | app()->config->set('honeystone-dto-tools.transformers', [ 309 | Bar::class => BarTransformer::class, 310 | Baz::class => BazTransformer::class, 311 | ]); 312 | 313 | $modified = Carbon::now(); 314 | 315 | $model = $this->partialMock(Foo::class, function (MockInterface $mock): void { 316 | $mock->shouldReceive('relationLoaded')->with('bar')->twice()->andReturn(true); 317 | $mock->shouldReceive('relationLoaded')->with('baz')->once()->andReturn(true); 318 | }); 319 | 320 | $model->fill([ 321 | 'id' => '1', 322 | 'name' => 'foo', 323 | 'description' => 'bar', 324 | 'featured' => 1, 325 | 'state' => 'draft', 326 | 'modified' => $modified, 327 | 328 | // relations 329 | 'bar' => new Bar(['id' => 1]), 330 | 'baz' => new EloquentCollection([new Baz(['id' => 1]), new Baz(['id' => 2])]), 331 | ]); 332 | 333 | $factory = new TransformerFactory([$model::class => RelationsTransformer::class]); 334 | 335 | $data = $factory->makeForModel($model)->transform($model); 336 | 337 | expect($data)->toBeInstanceOf(FooData::class) 338 | ->and($data->toArray())->tobe([ 339 | 'id' => 1, 340 | 'name' => 'foo', 341 | 'description' => 'bar', 342 | 'featured' => true, 343 | 'state' => State::DRAFT, 344 | 'modified' => $modified->toIso8601String(), 345 | 346 | // relations 347 | 'bar' => [ 348 | 'id' => 1, 349 | 'foobar' => null, 350 | ], 351 | 'baz' => [['id' => 1], ['id' => 2]], 352 | ]); 353 | }); 354 | 355 | it('transforms with loaded relations and a callback', function (): void { 356 | 357 | app()->config->set('honeystone-dto-tools.transformers', [ 358 | Bar::class => BarTransformer::class, 359 | Baz::class => BazTransformer::class, 360 | ]); 361 | 362 | $modified = Carbon::now(); 363 | 364 | $model = $this->partialMock(Foo::class, function (MockInterface $mock): void { 365 | $mock->shouldReceive('relationLoaded')->with('bar')->twice()->andReturn(true); 366 | $mock->shouldReceive('relationLoaded')->with('baz')->once()->andReturn(true); 367 | }); 368 | 369 | $model->fill([ 370 | 'id' => '1', 371 | 'name' => 'foo', 372 | 'description' => 'bar', 373 | 'featured' => 1, 374 | 'state' => 'draft', 375 | 'modified' => $modified, 376 | 377 | // relations 378 | 'bar' => new Bar(['id' => 1]), 379 | 'baz' => new EloquentCollection([new Baz(['id' => 1]), new Baz(['id' => 2])]), 380 | ]); 381 | 382 | $factory = new TransformerFactory([$model::class => RelationsTransformer::class]); 383 | 384 | $data = $factory->makeForModel($model)->transform($model, barExtra: ['callback' => function (Bar $model): void { 385 | $model->foobar = 'barbaz'; 386 | }]); 387 | 388 | expect($data)->toBeInstanceOf(FooData::class) 389 | ->and($data->toArray())->tobe([ 390 | 'id' => 1, 391 | 'name' => 'foo', 392 | 'description' => 'bar', 393 | 'featured' => true, 394 | 'state' => State::DRAFT, 395 | 'modified' => $modified->toIso8601String(), 396 | 397 | // relations 398 | 'bar' => [ 399 | 'id' => 1, 400 | 'foobar' => 'barbaz', 401 | ], 402 | 'baz' => [['id' => 1], ['id' => 2]], 403 | ]); 404 | }); 405 | 406 | it('transforms with loaded relations and exclusions', function (): void { 407 | 408 | app()->config->set('honeystone-dto-tools.transformers', [ 409 | Bar::class => BarTransformer::class, 410 | Baz::class => BazTransformer::class, 411 | ]); 412 | 413 | $modified = Carbon::now(); 414 | 415 | $model = $this->partialMock(Foo::class, function (MockInterface $mock): void { 416 | $mock->shouldReceive('relationLoaded')->with('bar')->twice()->andReturn(true); 417 | $mock->shouldReceive('relationLoaded')->with('baz')->once()->andReturn(true); 418 | }); 419 | 420 | $model->fill([ 421 | 'id' => '1', 422 | 'name' => 'foo', 423 | 'description' => 'bar', 424 | 'featured' => 1, 425 | 'state' => 'draft', 426 | 'modified' => $modified, 427 | 428 | // relations 429 | 'bar' => new Bar(['id' => 1, 'foobar' => 'barbaz']), 430 | 'baz' => new EloquentCollection([new Baz(['id' => 1]), new Baz(['id' => 2])]), 431 | ]); 432 | 433 | $factory = new TransformerFactory([$model::class => RelationsTransformer::class]); 434 | 435 | $data = $factory->makeForModel($model)->transform($model, barExtra: ['exclude' => ['foobar']]); 436 | 437 | expect($data)->toBeInstanceOf(FooData::class) 438 | ->and($data->toArray())->tobe([ 439 | 'id' => 1, 440 | 'name' => 'foo', 441 | 'description' => 'bar', 442 | 'featured' => true, 443 | 'state' => State::DRAFT, 444 | 'modified' => $modified->toIso8601String(), 445 | 446 | // relations 447 | 'bar' => [ 448 | 'id' => 1, 449 | 'foobar' => null, 450 | ], 451 | 'baz' => [['id' => 1], ['id' => 2]], 452 | ]); 453 | }); 454 | 455 | it('transforms with loaded relations and overrides', function (): void { 456 | 457 | app()->config->set('honeystone-dto-tools.transformers', [ 458 | Bar::class => BarTransformer::class, 459 | Baz::class => BazTransformer::class, 460 | ]); 461 | 462 | $modified = Carbon::now(); 463 | 464 | $model = $this->partialMock(Foo::class, function (MockInterface $mock): void { 465 | $mock->shouldReceive('relationLoaded')->with('bar')->twice()->andReturn(true); 466 | $mock->shouldReceive('relationLoaded')->with('baz')->once()->andReturn(true); 467 | }); 468 | 469 | $model->fill([ 470 | 'id' => '1', 471 | 'name' => 'foo', 472 | 'description' => 'bar', 473 | 'featured' => 1, 474 | 'state' => 'draft', 475 | 'modified' => $modified, 476 | 477 | // relations 478 | 'bar' => new Bar(['id' => 1, 'foobar' => 'barbaz']), 479 | 'baz' => new EloquentCollection([new Baz(['id' => 1]), new Baz(['id' => 2])]), 480 | ]); 481 | 482 | $factory = new TransformerFactory([$model::class => RelationsTransformer::class]); 483 | 484 | $data = $factory->makeForModel($model)->transform($model, barExtra: ['override' => ['foobar' => 'bar']]); 485 | 486 | expect($data)->toBeInstanceOf(FooData::class) 487 | ->and($data->toArray())->tobe([ 488 | 'id' => 1, 489 | 'name' => 'foo', 490 | 'description' => 'bar', 491 | 'featured' => true, 492 | 'state' => State::DRAFT, 493 | 'modified' => $modified->toIso8601String(), 494 | 495 | // relations 496 | 'bar' => [ 497 | 'id' => 1, 498 | 'foobar' => 'bar', 499 | ], 500 | 'baz' => [['id' => 1], ['id' => 2]], 501 | ]); 502 | }); 503 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 8 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | protected function getPackageProviders($app): array 17 | { 18 | return [DtoToolsServiceProvider::class]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/CreatableFromArray.php: -------------------------------------------------------------------------------- 1 | expect(TransformationData::make(['value' => 'foo'])->value) 10 | ->toBe('FOO'); 11 | 12 | it('creates from named args') 13 | ->expect(TransformationData::make(value: 'foo', data: ['bar'])->getAttributes()) 14 | ->toBe([ 15 | 'value' => 'FOO', 16 | 'data' => ['bar'], 17 | ]); 18 | 19 | it('transforms incoming') 20 | ->expect(TransformationData::make(value: 'foo')->value) 21 | ->toBe('FOO'); 22 | 23 | it('casts incoming to string') 24 | ->expect(CastingData::make(default: 888)->default) 25 | ->toBe('888'); 26 | 27 | it('casts incoming to int') 28 | ->expect(CastingData::make(stringInt: false)->stringInt) 29 | ->toBe(0); 30 | 31 | it('keeps incoming string') 32 | ->expect(CastingData::make(stringInt: 'foo')->stringInt) 33 | ->toBe('foo'); 34 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/CreatableFromSnakeTest.php: -------------------------------------------------------------------------------- 1 | expect(SnakeTransformationData::make(multi_word_parameter: 'foo')->multiWordParameter) 9 | ->toBe('foo'); 10 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/Fixtures/CastingData.php: -------------------------------------------------------------------------------- 1 | 'int|null'])] 13 | #[ToMany(['children' => 'int|empty|null'])] 14 | final class CreationData implements Storable 15 | { 16 | use HasStorableData; 17 | 18 | /** 19 | * @param array $baz 20 | */ 21 | public function __construct( 22 | public readonly ?string $foo = null, 23 | public readonly ?int $bar = null, 24 | public readonly array $baz = [], 25 | ) {} 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/Fixtures/KeyTransformationData.php: -------------------------------------------------------------------------------- 1 | 'int|null'])] 15 | #[ToMany(['children' => 'int|empty|null'])] 16 | final class PatchData implements Storable 17 | { 18 | use HasStorableData; 19 | 20 | /** 21 | * @param array $baz 22 | */ 23 | public function __construct( 24 | public readonly ?string $foo = null, 25 | public readonly ?int $bar = null, 26 | public readonly array $baz = [], 27 | ) {} 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/Fixtures/SnakeTransformationData.php: -------------------------------------------------------------------------------- 1 | $parameters 21 | * 22 | * @return array 23 | */ 24 | protected static function transformIncoming(array $parameters): array 25 | { 26 | return [ 27 | 'value' => strtoupper($parameters['value'] ?? ''), 28 | 'data' => $parameters['data'] ?? null, 29 | ]; 30 | } 31 | 32 | /** 33 | * @param array $parameters 34 | * 35 | * @return array 36 | */ 37 | protected function transformOutgoing(array $parameters): array 38 | { 39 | return [ 40 | 'transformed' => $parameters['value'], 41 | 'data' => $parameters['data'], 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/HasStorableDataTest.php: -------------------------------------------------------------------------------- 1 | expect(PatchData::make()->relationships()) 11 | ->toBeInstanceOf(StorableRelationships::class); 12 | 13 | it('supports creation instances') 14 | ->expect(CreationData::make()->isPatching()) 15 | ->toBeFalse(); 16 | 17 | it('supports patching instances') 18 | ->expect(PatchData::make()->isPatching()) 19 | ->toBeTrue(); 20 | 21 | it('reports all attributes as storable in creation mode', function (): void { 22 | 23 | $data = CreationData::make(foo: ':-)'); 24 | 25 | expect($data->isStorable('foo'))->toBeTrue() 26 | ->and($data->isStorable('bar'))->toBeTrue() 27 | ->and($data->isStorable('baz'))->toBeTrue(); 28 | }); 29 | 30 | it('reports all relationships as storable in creation mode', function (): void { 31 | 32 | $data = CreationData::make(); 33 | 34 | expect($data->isStorable('parent'))->toBeTrue() 35 | ->and($data->isStorable('children'))->toBeTrue(); 36 | }); 37 | 38 | it('reports non-null attributes as storable in patch mode', function (): void { 39 | 40 | $data = PatchData::make(foo: ':-)'); 41 | 42 | expect($data->isStorable('foo'))->toBeTrue() 43 | ->and($data->isStorable('bar'))->toBeFalse() 44 | ->and($data->isStorable('baz'))->toBeTrue(); 45 | }); 46 | 47 | it('reports non-null relationships as storable in patch mode', function (): void { 48 | 49 | $data = PatchData::make(); 50 | 51 | $data->relationships()->setOneRelated('parent', 10); 52 | 53 | expect($data->isStorable('parent'))->toBeTrue() 54 | ->and($data->isStorable('children'))->toBeFalse(); 55 | }); 56 | 57 | it('reports forced null attributes as storable in patch mode', function (): void { 58 | 59 | $data = PatchData::make(foo: ':-)'); 60 | 61 | $data->force('bar'); 62 | 63 | expect($data->isStorable('foo'))->toBeTrue() 64 | ->and($data->isStorable('bar'))->toBeTrue() 65 | ->and($data->isStorable('baz'))->toBeTrue(); 66 | }); 67 | 68 | it('reports forced null relationships as storable in patch mode', function (): void { 69 | 70 | $data = PatchData::make(); 71 | 72 | $data->relationships()->setOneRelated('parent', 10); 73 | 74 | $data->force('children'); 75 | 76 | expect($data->isStorable('parent'))->toBeTrue() 77 | ->and($data->isStorable('children'))->toBeTrue(); 78 | }); 79 | 80 | it('outputs serialised storable patch data', function (): void { 81 | 82 | $data = PatchData::make(foo: ':-)'); 83 | 84 | $data->relationships()->setOneRelated('parent', 10); 85 | $data->relationships()->replaceToMany('children', [20, 25, 30]); 86 | 87 | expect($data->toStorableArray()) 88 | ->toBe([ 89 | 'foo' => ':-)', 90 | 'baz' => [], 91 | 'parent' => 10, 92 | 'children' => [20, 25, 30], 93 | ]); 94 | }); 95 | 96 | it('outputs serialised storable creation data', function (): void { 97 | 98 | $data = CreationData::make(foo: ':-)'); 99 | 100 | $data->relationships()->setOneRelated('parent', 10); 101 | $data->relationships()->replaceToMany('children', [20, 25, 30]); 102 | 103 | expect($data->toStorableArray()) 104 | ->toBe([ 105 | 'foo' => ':-)', 106 | 'bar' => null, 107 | 'baz' => [], 108 | 'parent' => 10, 109 | 'children' => [20, 25, 30], 110 | ]); 111 | }); 112 | 113 | it('includes forced null values in serialised storable patch data', function (): void { 114 | 115 | $data = PatchData::make(foo: ':-)'); 116 | 117 | $data->relationships()->setOneRelated('parent', 10); 118 | $data->relationships()->replaceToMany('children', [20, 25, 30]); 119 | 120 | $data->force('bar'); 121 | 122 | expect($data->toStorableArray()) 123 | ->toBe([ 124 | 'foo' => ':-)', 125 | 'bar' => null, 126 | 'baz' => [], 127 | 'parent' => 10, 128 | 'children' => [20, 25, 30], 129 | ]); 130 | }); 131 | 132 | it('retrieves serialised relationships', function (): void { 133 | 134 | $data = PatchData::make(); 135 | 136 | $data->relationships()->setOneRelated('parent', 10); 137 | $data->relationships()->replaceToMany('children', [20, 25, 30]); 138 | 139 | expect($data->getRelationships()) 140 | ->toBe(['parent' => 10, 'children' => [20, 25, 30]]); 141 | }); 142 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/SerialisesToArrayTest.php: -------------------------------------------------------------------------------- 1 | expect(TransformationData::make(data: TransformationData::make(value: 'foo'))->toArray()) 10 | ->toBe([ 11 | 'transformed' => '', 12 | 'data' => [ 13 | 'transformed' => 'FOO', 14 | 'data' => null, 15 | ], 16 | ]); 17 | 18 | it('transforms nested arrayables') 19 | ->expect(TransformationData::make(data: [ 20 | 'foo' => [ 21 | 'bar' => TransformationData::make(value: 'baz'), 22 | ], 23 | ], 24 | )->toArray()) 25 | ->toBe([ 26 | 'transformed' => '', 27 | 'data' => [ 28 | 'foo' => [ 29 | 'bar' => [ 30 | 'transformed' => 'BAZ', 31 | 'data' => null, 32 | ], 33 | ], 34 | ], 35 | ]); 36 | 37 | it('transforms outgoing') 38 | ->expect(TransformationData::make(value: 'bar')->toArray()) 39 | ->toBe([ 40 | 'transformed' => 'BAR', 41 | 'data' => null, 42 | ]); 43 | 44 | it('allows transformations to be bypassed') 45 | ->expect(TransformationData::make(value: 'bar')->toRawArray()) 46 | ->toBe([ 47 | 'value' => 'BAR', 48 | 'data' => null, 49 | ]); 50 | 51 | it('skips the key property') 52 | ->expect(KeyTransformationData::make(id: 8)->toRawArray()) 53 | ->toBe([ 54 | 'id' => 8, 55 | 'value' => 'Foo', 56 | ]); 57 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/SerialisesToSnakeTest.php: -------------------------------------------------------------------------------- 1 | expect(SnakeTransformationData::make(multiWordParameter: 'foo')->toArray()) 9 | ->toBe(['multi_word_parameter' => 'foo']); 10 | -------------------------------------------------------------------------------- /tests/Unit/Fixtures/FooData.php: -------------------------------------------------------------------------------- 1 | 'int|null'], ['children' => 'int|empty'])) 13 | ->toBeInstanceOf(Relationships::class); 14 | }); 15 | 16 | it('constructs without relationship definitions', function (): void { 17 | 18 | expect(new Relationships([], []))->toBeInstanceOf(Relationships::class); 19 | }); 20 | 21 | it('allows valid to-one transferable type', function (): void { 22 | 23 | $relationships = new Relationships(['foo' => 'int|string|null'], []); 24 | $relationships->setOneRelated('foo', FooData::make(id: 8)); 25 | 26 | expect($relationships->foo)->toBe(8); 27 | }); 28 | 29 | it('allows valid to-one int type', function (): void { 30 | 31 | $relationships = new Relationships(['foo' => 'int|string|null'], []); 32 | $relationships->setOneRelated('foo', 8); 33 | 34 | expect($relationships->foo)->toBe(8); 35 | }); 36 | 37 | it('blocks invalid to-one int type', function (): void { 38 | 39 | $relationships = new Relationships(['foo' => 'string|null'], []); 40 | $relationships->setOneRelated('foo', 8); 41 | 42 | })->throws( 43 | RelationValidationException::class, 44 | 'The related id should be of the type string|null, instead the id was `8` (integer).', 45 | ); 46 | 47 | it('allows valid to-one string type', function (): void { 48 | 49 | $relationships = new Relationships(['foo' => 'int|string|null'], []); 50 | $relationships->setOneRelated('foo', '8'); 51 | 52 | expect($relationships->foo)->toBe('8'); 53 | }); 54 | 55 | it('blocks invalid to-one string type', function (): void { 56 | 57 | $relationships = new Relationships(['foo' => 'int|null'], []); 58 | $relationships->setOneRelated('foo', '8'); 59 | 60 | })->throws( 61 | RelationValidationException::class, 62 | 'The related id should be of the type integer|null, instead the id was `8` (string).', 63 | ); 64 | 65 | it('allows valid to-one null value', function (): void { 66 | 67 | $relationships = new Relationships(['foo' => 'int|string|null'], []); 68 | $relationships->setOneRelated('foo', null); 69 | 70 | expect($relationships->foo)->toBeNull(); 71 | }); 72 | 73 | it('blocks invalid to-one null value', function (): void { 74 | 75 | $relationships = new Relationships(['foo' => 'int|string'], []); 76 | $relationships->setOneRelated('foo', null); 77 | 78 | })->throws( 79 | RelationValidationException::class, 80 | 'The related id should be of the type integer|string, instead the id was `` (null).', 81 | ); 82 | 83 | it('allows valid to-many transferable type', function (): void { 84 | 85 | $relationships = new Relationships([], ['bar' => 'int|string|empty']); 86 | $relationships->replaceToMany('bar', [FooData::make(id: 8)]); 87 | 88 | expect($relationships->bar)->toBe([8]); 89 | }); 90 | 91 | it('allows valid to-many int type', function (): void { 92 | 93 | $relationships = new Relationships([], ['bar' => 'int|string|empty']); 94 | $relationships->replaceToMany('bar', [8]); 95 | 96 | expect($relationships->bar)->toBe([8]); 97 | }); 98 | 99 | it('blocks invalid to-many int type', function (): void { 100 | 101 | $relationships = new Relationships([], ['bar' => 'string|empty']); 102 | $relationships->replaceToMany('bar', [8]); 103 | 104 | })->throws( 105 | RelationValidationException::class, 106 | 'The related id should be of the type string|empty, instead the id was `8` (integer).', 107 | ); 108 | 109 | it('allows valid to-many string type', function (): void { 110 | 111 | $relationships = new Relationships([], ['bar' => 'int|string|empty']); 112 | $relationships->replaceToMany('bar', ['8']); 113 | 114 | expect($relationships->bar)->toBe(['8']); 115 | }); 116 | 117 | it('blocks invalid to-many string type', function (): void { 118 | 119 | $relationships = new Relationships([], ['bar' => 'int|empty']); 120 | $relationships->replaceToMany('bar', ['8']); 121 | 122 | })->throws( 123 | RelationValidationException::class, 124 | 'The related id should be of the type integer|empty, instead the id was `8` (string).', 125 | ); 126 | 127 | it('allows valid empty to-many', function (): void { 128 | 129 | $relationships = new Relationships([], ['bar' => 'int|string|empty']); 130 | $relationships->replaceToMany('bar', []); 131 | 132 | expect($relationships->bar)->toBe([]); 133 | }); 134 | 135 | it('blocks invalid empty to-many', function (): void { 136 | 137 | $relationships = new Relationships([], ['bar' => 'int|string']); 138 | $relationships->replaceToMany('bar', []); 139 | 140 | })->throws( 141 | RelationValidationException::class, 142 | 'The bar relationship must not be empty.', 143 | ); 144 | 145 | it('allows valid empty to-many reset', function (): void { 146 | 147 | $relationships = new Relationships([], ['bar' => 'int|string|empty']); 148 | $relationships->resetToMany('bar'); 149 | 150 | expect($relationships->bar)->toBe([]) 151 | ->and($relationships->hasToMany('bar'))->toBeFalse(); 152 | }); 153 | 154 | it('blocks invalid empty to-many reset', function (): void { 155 | 156 | $relationships = new Relationships([], ['bar' => 'int|string']); 157 | $relationships->replaceToMany('bar', []); 158 | 159 | })->throws( 160 | RelationValidationException::class, 161 | 'The bar relationship must not be empty.', 162 | ); 163 | 164 | it('allows valid to-many null reset', function (): void { 165 | 166 | $relationships = new Relationships([], ['bar' => 'int|string|empty|null']); 167 | $relationships->resetToMany('bar'); 168 | 169 | expect($relationships->bar)->toBeNull(); 170 | }); 171 | 172 | it('blocks invalid to-many null reset', function (): void { 173 | 174 | $relationships = new Relationships([], ['bar' => 'int|string']); 175 | $relationships->resetToMany('bar'); 176 | 177 | })->throws( 178 | RelationValidationException::class, 179 | 'The bar relationship must be set.', 180 | ); 181 | 182 | it('unsets to-one relationships', function (): void { 183 | 184 | $relationships = new Relationships(['foo' => 'int|string|null'], []); 185 | $relationships->setOneRelated('foo', 8); 186 | $relationships->unsetOneRelated('foo'); 187 | 188 | expect($relationships->foo)->toBeNull(); 189 | }); 190 | 191 | it('retrieves to-one relationship values', function (): void { 192 | 193 | $relationships = new Relationships(['foo' => 'string'], []); 194 | $relationships->setOneRelated('foo', 'bar'); 195 | 196 | expect($relationships->getOneRelated('foo'))->toBe('bar'); 197 | }); 198 | 199 | it('retrieves to-many relationship values', function (): void { 200 | 201 | $relationships = new Relationships([], ['foo' => 'string']); 202 | $relationships->replaceToMany('foo', ['bar']); 203 | 204 | expect($relationships->getManyRelated('foo'))->toBe(['bar']); 205 | }); 206 | 207 | it('retrieves to-many relationship null values', function (): void { 208 | 209 | $relationships = new Relationships([], ['foo' => 'string|null']); 210 | $relationships->replaceToMany('foo', null); 211 | 212 | expect($relationships->getManyRelated('foo'))->toBeNull(); 213 | }); 214 | 215 | it('magically retrieves to-one relationship values', function (): void { 216 | 217 | $relationships = new Relationships(['foo' => 'string'], []); 218 | $relationships->setOneRelated('foo', 'bar'); 219 | 220 | expect($relationships->foo)->toBe('bar'); 221 | }); 222 | 223 | it('magically retrieves to-many relationship values', function (): void { 224 | 225 | $relationships = new Relationships([], ['foo' => 'string']); 226 | $relationships->replaceToMany('foo', ['bar']); 227 | 228 | expect($relationships->foo)->toBe(['bar']); 229 | }); 230 | 231 | it('magically sets to-one relationship values', function (): void { 232 | 233 | $relationships = new Relationships(['foo' => 'string'], []); 234 | $relationships->foo = 'bar'; 235 | 236 | expect($relationships->getOneRelated('foo'))->toBe('bar'); 237 | }); 238 | 239 | it('magically replaces to-many relationship values', function (): void { 240 | 241 | $relationships = new Relationships([], ['foo' => 'string']); 242 | $relationships->foo = ['bar']; 243 | 244 | expect($relationships->getManyRelated('foo'))->toBe(['bar']); 245 | }); 246 | 247 | it('checks if we have a to-one relationship value', function (): void { 248 | 249 | $relationships = new Relationships(['foo' => 'string', 'bar' => 'string|null', 'baz' => 'string|null'], []); 250 | $relationships->setOneRelated('foo', ':-)'); 251 | $relationships->setOneRelated('bar', null); 252 | 253 | expect($relationships->hasToOne('foo'))->ToBeTrue() 254 | ->and($relationships->hasToOne('bar'))->ToBeTrue() 255 | ->and($relationships->hasToOne('baz'))->ToBeFalse(); 256 | }); 257 | 258 | it('checks if we have a to-many relationship replacement', function (): void { 259 | 260 | $relationships = new Relationships([], [ 261 | 'foo' => 'string', 262 | 'bar' => 'string|empty|null', 263 | 'baz' => 'string|empty|null', 264 | 'foobar' => 'string', 265 | ]); 266 | 267 | $relationships->replaceToMany('foo', [':-)']); 268 | $relationships->replaceToMany('bar', null); 269 | $relationships->replaceToMany('baz'); 270 | 271 | expect($relationships->hasToMany('foo'))->ToBeTrue() 272 | ->and($relationships->hasToMany('bar'))->ToBeTrue() 273 | ->and($relationships->hasToMany('baz'))->ToBeTrue() 274 | ->and($relationships->hasToMany('foobar'))->ToBeFalse(); 275 | }); 276 | 277 | it('magically checks if we have a to-one relationship value', function (): void { 278 | 279 | $relationships = new Relationships(['foo' => 'string', 'bar' => 'string'], []); 280 | $relationships->setOneRelated('foo', ':-)'); 281 | 282 | expect(isset($relationships->foo))->toBeTrue() 283 | ->and(isset($relationships->bar))->toBeFalse(); 284 | }); 285 | 286 | it('magically checks if we have a to-many relationship replacement', function (): void { 287 | 288 | $relationships = new Relationships([], ['foo' => 'string', 'bar' => 'string']); 289 | $relationships->replaceToMany('foo', [':-)']); 290 | 291 | expect(isset($relationships->foo))->toBeTrue() 292 | ->and(isset($relationships->bar))->toBeFalse(); 293 | }); 294 | 295 | it('returns null if we don\'t have a to-one relationship value', function (): void { 296 | 297 | $relationships = new Relationships(['foo' => 'string'], []); 298 | 299 | expect($relationships->getOneRelated('foo'))->toBeNull(); 300 | }); 301 | 302 | it('returns null if we don\'t have a to-many relationship value', function (): void { 303 | 304 | $relationships = new Relationships([], ['foo' => 'string']); 305 | 306 | expect($relationships->getManyRelated('foo'))->toBe([]); 307 | }); 308 | 309 | it('merges additions and extracts removals when we retrieve a to-many replacement', function (): void { 310 | 311 | $relationships = new Relationships([], ['foo' => 'string']); 312 | $relationships->replaceToMany('foo', [':-)', '¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)']); 313 | $relationships->addToManyRelation('foo', FooData::make(id: 'ʕ•ᴥ•ʔ')); 314 | $relationships->removeToManyRelation('foo', ':-)'); 315 | 316 | expect($relationships->getManyRelated('foo'))->toBe(['¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)', 'ʕ•ᴥ•ʔ']); 317 | }); 318 | 319 | it('merges additions and extracts removals when retrieving a to-many replacement', function (): void { 320 | 321 | $relationships = new Relationships([], ['foo' => 'string']); 322 | $relationships->replaceToMany('foo', [':-)', '¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)']); 323 | $relationships->addToManyRelation('foo', 'ʕ•ᴥ•ʔ'); 324 | $relationships->removeToManyRelation('foo', ':-)'); 325 | 326 | expect($relationships->getManyRelated('foo'))->toBe(['¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)', 'ʕ•ᴥ•ʔ']) 327 | ->and($relationships->foo)->toBe(['¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)', 'ʕ•ᴥ•ʔ']); 328 | }); 329 | 330 | it('excludes additions and removals when attempting to get a to-many replacement that has not been set', function (): void { 331 | 332 | $relationships = new Relationships([], ['foo' => 'string']); 333 | $relationships->addToManyRelation('foo', 'ʕ•ᴥ•ʔ'); 334 | $relationships->removeToManyRelation('foo', FooData::make(id: ':-)')); 335 | 336 | expect($relationships->getManyRelated('foo'))->toBe([]) 337 | ->and($relationships->foo)->toBe([]); 338 | }); 339 | 340 | it('stores to-many relationship additions', function (): void { 341 | 342 | $relationships = new Relationships([], ['foo' => 'int|string|null']); 343 | $relationships->addToManyRelation('foo', 8); 344 | 345 | expect($relationships->getManyAdditions('foo'))->toBe([8]); 346 | }); 347 | 348 | it('stores to-many relationship removals', function (): void { 349 | 350 | $relationships = new Relationships([], ['foo' => 'int|string|null']); 351 | $relationships->removeToManyRelation('foo', FooData::make(id: 8)); 352 | 353 | expect($relationships->getManyRemovals('foo'))->toBe([8]); 354 | }); 355 | 356 | it('stores to-many relationship addition transferable pivot data', function (): void { 357 | 358 | $relationships = new Relationships([], ['foo' => 'int|string|null']); 359 | $relationships->addToManyRelation('foo', 8, MetaData::make(['order' => 0, 'title' => 'Foo'])); 360 | 361 | expect($relationships->getMetaData('foo'))->toBe([8 => ['order' => 0, 'title' => 'Foo']]); 362 | }); 363 | 364 | it('stores to-many relationship addition pivot data', function (): void { 365 | 366 | $relationships = new Relationships([], ['foo' => 'int|string|null']); 367 | $relationships->addToManyRelation('foo', 8, ['order' => 0, 'Title' => 'Foo']); 368 | 369 | expect($relationships->getMetaData('foo'))->toBe([8 => ['order' => 0, 'Title' => 'Foo']]); 370 | }); 371 | 372 | it('stores to-many relationship replacement pivot data', function (): void { 373 | 374 | $pivot = [ 375 | ':-)' => ['order' => 0, 'Title' => 'Smile'], 376 | '¯\_(ツ)_/¯' => ['order' => 1, 'Title' => 'Meh'], 377 | '( ͡° ͜ʖ ͡°)' => ['order' => 2, 'Title' => 'Glance'], 378 | ]; 379 | 380 | $relationships = new Relationships([], ['foo' => 'int|string|null']); 381 | $relationships->replaceToMany('foo', [':-)', '¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)'], $pivot); 382 | 383 | expect($relationships->getMetaData('foo'))->toBe($pivot); 384 | }); 385 | 386 | it('retrieves to-many relationship mixed pivot data', function (): void { 387 | 388 | $relationships = new Relationships([], ['foo' => 'int|string|null']); 389 | $relationships->replaceToMany('foo', [':-)', '¯\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)'], [ 390 | ':-)' => ['order' => 0, 'Title' => 'Smile'], 391 | '¯\_(ツ)_/¯' => ['order' => 1, 'Title' => 'Meh'], 392 | '( ͡° ͜ʖ ͡°)' => ['order' => 2, 'Title' => 'Glance'], 393 | ]); 394 | $relationships->addToManyRelation('foo', 8, ['order' => 3, 'Title' => 'Foo']); 395 | 396 | expect($relationships->getMetaData('foo'))->toBe([ 397 | ':-)' => ['order' => 0, 'Title' => 'Smile'], 398 | '¯\_(ツ)_/¯' => ['order' => 1, 'Title' => 'Meh'], 399 | '( ͡° ͜ʖ ͡°)' => ['order' => 2, 'Title' => 'Glance'], 400 | 8 => ['order' => 3, 'Title' => 'Foo'], 401 | ]); 402 | }); 403 | 404 | it('serialises to an array', function (): void { 405 | 406 | $relationships = new Relationships(['foo' => 'string'], ['bar' => 'string']); 407 | $relationships->setOneRelated('foo', ':-)'); 408 | $relationships->replaceToMany('bar', ['¯\_(ツ)_/¯']); 409 | $relationships->addToManyRelation('bar', 'ʕ•ᴥ•ʔ'); 410 | 411 | expect($relationships->toArray())->toBe(['foo' => ':-)', 'bar' => ['¯\_(ツ)_/¯', 'ʕ•ᴥ•ʔ']]); 412 | }); 413 | -------------------------------------------------------------------------------- /workbench/app/Models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honeystone/laravel-dto-tools/8a0566a68fc0a014e3193644b3067360129aff95/workbench/app/Models/.gitkeep -------------------------------------------------------------------------------- /workbench/app/Providers/WorkbenchServiceProvider.php: -------------------------------------------------------------------------------- 1 | withRouting( 13 | web: __DIR__.'/../routes/web.php', 14 | commands: __DIR__.'/../routes/console.php', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware) { 17 | // 18 | }) 19 | ->withExceptions(function (Exceptions $exceptions) { 20 | // 21 | })->create(); 22 | -------------------------------------------------------------------------------- /workbench/database/factories/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honeystone/laravel-dto-tools/8a0566a68fc0a014e3193644b3067360129aff95/workbench/database/factories/.gitkeep -------------------------------------------------------------------------------- /workbench/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honeystone/laravel-dto-tools/8a0566a68fc0a014e3193644b3067360129aff95/workbench/database/migrations/.gitkeep -------------------------------------------------------------------------------- /workbench/database/seeders/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honeystone/laravel-dto-tools/8a0566a68fc0a014e3193644b3067360129aff95/workbench/database/seeders/.gitkeep -------------------------------------------------------------------------------- /workbench/database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 10 | })->purpose('Display an inspiring quote')->hourly(); 11 | -------------------------------------------------------------------------------- /workbench/routes/web.php: -------------------------------------------------------------------------------- 1 |