├── .gitignore ├── .idea ├── inspectionProfiles │ └── Project_Default.xml ├── ipc19se-writing-really-good-code.iml ├── misc.xml ├── modules.xml ├── php-inspections-ea-ultimate.xml ├── php.xml └── vcs.xml ├── .php_cs.dist ├── README.md ├── doc └── prices.png ├── phive.xml ├── phpunit.xml ├── psalm.xml ├── src ├── Exception.php ├── Good.php ├── Market.php ├── Milk.php ├── Offer.php ├── OutOfRangeException.php ├── Pound.php ├── PriceList.php ├── PriceListBuilder.php ├── Quantity.php └── autoload.php ├── tests ├── GoodTest.php ├── MarketTest.php ├── OfferTest.php ├── PoundTest.php ├── PriceListBuilderTest.php ├── PriceListTest.php └── QuantityTest.php └── tools ├── php-cs-fixer ├── phpab ├── phpunit ├── phpunit.phar └── psalm /.gitignore: -------------------------------------------------------------------------------- 1 | .php_cs.cache 2 | .phpunit.result.cache 3 | 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Generated files 15 | .idea/**/contentModel.xml 16 | 17 | # Sensitive or high-churn files 18 | .idea/**/dataSources/ 19 | .idea/**/dataSources.ids 20 | .idea/**/dataSources.local.xml 21 | .idea/**/sqlDataSources.xml 22 | .idea/**/dynamic.xml 23 | .idea/**/uiDesigner.xml 24 | .idea/**/dbnavigator.xml 25 | 26 | # Gradle 27 | .idea/**/gradle.xml 28 | .idea/**/libraries 29 | 30 | # Gradle and Maven with auto-import 31 | # When using Gradle or Maven with auto-import, you should exclude module files, 32 | # since they will be recreated, and may cause churn. Uncomment if using 33 | # auto-import. 34 | # .idea/modules.xml 35 | # .idea/*.iml 36 | # .idea/modules 37 | 38 | # CMake 39 | cmake-build-*/ 40 | 41 | # Mongo Explorer plugin 42 | .idea/**/mongoSettings.xml 43 | 44 | # File-based project format 45 | *.iws 46 | 47 | # IntelliJ 48 | out/ 49 | 50 | # mpeltonen/sbt-idea plugin 51 | .idea_modules/ 52 | 53 | # JIRA plugin 54 | atlassian-ide-plugin.xml 55 | 56 | # Cursive Clojure plugin 57 | .idea/replstate.xml 58 | 59 | # Crashlytics plugin (for Android Studio and IntelliJ) 60 | com_crashlytics_export_strings.xml 61 | crashlytics.properties 62 | crashlytics-build.properties 63 | fabric.properties 64 | 65 | # Editor-based Rest Client 66 | .idea/httpRequests 67 | 68 | # Android studio 3.1+ serialized cache file 69 | .idea/caches/build_file_checksums.ser 70 | /website/public/css/styles.css 71 | /website/public/css/styles.css.map 72 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 483 | -------------------------------------------------------------------------------- /.idea/ipc19se-writing-really-good-code.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/php-inspections-ea-ultimate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 4 | ->setRules( 5 | [ 6 | 'align_multiline_comment' => true, 7 | 'array_indentation' => true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'binary_operator_spaces' => [ 10 | 'operators' => [ 11 | '=' => 'align', 12 | '=>' => 'align', 13 | ], 14 | ], 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_before_statement' => [ 17 | 'statements' => [ 18 | 'break', 19 | 'continue', 20 | 'declare', 21 | 'do', 22 | 'for', 23 | 'foreach', 24 | 'if', 25 | 'include', 26 | 'include_once', 27 | 'require', 28 | 'require_once', 29 | 'return', 30 | 'switch', 31 | 'throw', 32 | 'try', 33 | 'while', 34 | 'yield', 35 | ], 36 | ], 37 | 'braces' => true, 38 | 'cast_spaces' => true, 39 | 'class_attributes_separation' => ['elements' => ['const', 'method', 'property']], 40 | 'combine_consecutive_issets' => true, 41 | 'combine_consecutive_unsets' => true, 42 | 'compact_nullable_typehint' => true, 43 | 'concat_space' => ['spacing' => 'one'], 44 | 'declare_equal_normalize' => ['space' => 'none'], 45 | 'declare_strict_types' => true, 46 | 'dir_constant' => true, 47 | 'elseif' => true, 48 | 'encoding' => true, 49 | 'full_opening_tag' => true, 50 | 'function_declaration' => true, 51 | 'indentation_type' => true, 52 | 'is_null' => true, 53 | 'line_ending' => true, 54 | 'list_syntax' => ['syntax' => 'short'], 55 | 'logical_operators' => true, 56 | 'lowercase_cast' => true, 57 | 'lowercase_constants' => true, 58 | 'lowercase_keywords' => true, 59 | 'lowercase_static_reference' => true, 60 | 'magic_constant_casing' => true, 61 | 'method_argument_space' => ['ensure_fully_multiline' => true], 62 | 'modernize_types_casting' => true, 63 | 'multiline_comment_opening_closing' => true, 64 | 'multiline_whitespace_before_semicolons' => true, 65 | 'native_constant_invocation' => true, 66 | 'native_function_casing' => true, 67 | 'native_function_invocation' => true, 68 | 'new_with_braces' => false, 69 | 'no_alias_functions' => true, 70 | 'no_alternative_syntax' => true, 71 | 'no_blank_lines_after_class_opening' => true, 72 | 'no_blank_lines_after_phpdoc' => true, 73 | 'no_blank_lines_before_namespace' => true, 74 | 'no_closing_tag' => true, 75 | 'no_empty_comment' => true, 76 | 'no_empty_phpdoc' => true, 77 | 'no_empty_statement' => true, 78 | 'no_extra_blank_lines' => true, 79 | 'no_homoglyph_names' => true, 80 | 'no_leading_import_slash' => true, 81 | 'no_leading_namespace_whitespace' => true, 82 | 'no_mixed_echo_print' => ['use' => 'print'], 83 | 'no_multiline_whitespace_around_double_arrow' => true, 84 | 'no_null_property_initialization' => true, 85 | 'no_php4_constructor' => true, 86 | 'no_short_bool_cast' => true, 87 | 'no_short_echo_tag' => true, 88 | 'no_singleline_whitespace_before_semicolons' => true, 89 | 'no_spaces_after_function_name' => true, 90 | 'no_spaces_inside_parenthesis' => true, 91 | 'no_superfluous_elseif' => true, 92 | 'no_superfluous_phpdoc_tags' => true, 93 | 'no_trailing_comma_in_list_call' => true, 94 | 'no_trailing_comma_in_singleline_array' => true, 95 | 'no_trailing_whitespace' => true, 96 | 'no_trailing_whitespace_in_comment' => true, 97 | 'no_unneeded_control_parentheses' => true, 98 | 'no_unneeded_curly_braces' => true, 99 | 'no_unneeded_final_method' => true, 100 | 'no_unreachable_default_argument_value' => true, 101 | 'no_unset_on_property' => true, 102 | 'no_unused_imports' => true, 103 | 'no_useless_else' => true, 104 | 'no_useless_return' => true, 105 | 'no_whitespace_before_comma_in_array' => true, 106 | 'no_whitespace_in_blank_line' => true, 107 | 'non_printable_character' => true, 108 | 'normalize_index_brace' => true, 109 | 'object_operator_without_whitespace' => true, 110 | 'ordered_class_elements' => [ 111 | 'order' => [ 112 | 'use_trait', 113 | 'constant_public', 114 | 'constant_protected', 115 | 'constant_private', 116 | 'property_public_static', 117 | 'property_protected_static', 118 | 'property_private_static', 119 | 'property_public', 120 | 'property_protected', 121 | 'property_private', 122 | 'method_public_static', 123 | 'construct', 124 | 'destruct', 125 | 'magic', 126 | 'phpunit', 127 | 'method_public', 128 | 'method_protected', 129 | 'method_private', 130 | 'method_protected_static', 131 | 'method_private_static', 132 | ], 133 | ], 134 | 'ordered_imports' => true, 135 | 'ordered_interfaces' => [ 136 | 'direction' => 'ascend', 137 | 'order' => 'alpha', 138 | ], 139 | 'phpdoc_add_missing_param_annotation' => true, 140 | 'phpdoc_align' => true, 141 | 'phpdoc_annotation_without_dot' => true, 142 | 'phpdoc_indent' => true, 143 | 'phpdoc_no_access' => true, 144 | 'phpdoc_no_empty_return' => true, 145 | 'phpdoc_no_package' => true, 146 | 'phpdoc_order' => true, 147 | 'phpdoc_return_self_reference' => true, 148 | 'phpdoc_scalar' => true, 149 | 'phpdoc_separation' => true, 150 | 'phpdoc_single_line_var_spacing' => true, 151 | 'phpdoc_to_comment' => true, 152 | 'phpdoc_trim' => true, 153 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 154 | 'phpdoc_types' => ['groups' => ['simple', 'meta']], 155 | 'phpdoc_types_order' => true, 156 | 'phpdoc_var_without_name' => true, 157 | 'pow_to_exponentiation' => true, 158 | 'protected_to_private' => true, 159 | 'return_assignment' => true, 160 | 'return_type_declaration' => ['space_before' => 'none'], 161 | 'self_accessor' => true, 162 | 'semicolon_after_instruction' => true, 163 | 'set_type_to_cast' => true, 164 | 'short_scalar_cast' => true, 165 | 'simplified_null_return' => true, 166 | 'single_blank_line_at_eof' => true, 167 | 'single_import_per_statement' => true, 168 | 'single_line_after_imports' => true, 169 | 'single_quote' => true, 170 | 'standardize_not_equals' => true, 171 | 'ternary_to_null_coalescing' => true, 172 | 'trailing_comma_in_multiline_array' => true, 173 | 'trim_array_spaces' => true, 174 | 'unary_operator_spaces' => true, 175 | 'visibility_required' => [ 176 | 'elements' => [ 177 | 'const', 178 | 'method', 179 | 'property', 180 | ], 181 | ], 182 | 'void_return' => true, 183 | 'whitespace_after_comma_in_array' => true, 184 | ] 185 | ) 186 | ->setFinder( 187 | PhpCsFixer\Finder::create() 188 | ->files() 189 | ->in(__DIR__ . '/src') 190 | ->in(__DIR__ . '/tests') 191 | ->notName('autoload.php') 192 | ); 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Writing Really Good Code 2 | ## Sebastian Bergmann and Arne Blankerts 3 | 4 | > What makes up clean code? How does code turn out well? And how do you write really good code? In this workshop you will not only learn the answers to these questions. You will have the opportunity to immediately apply what you learn in a practical exercise. First, we will show how Domain-Driven Design and Test-Driven Development can be used to solve problems. Needless to say that we will cover topics such as Clean Code and SOLID along the way. Coached by the trainers, you will then work in pairs on additional features for the software we developed during the live coding. We will round up the day with a review where you will get feedback on the code you created. You will need to bring your own laptop to really benefit from this workshop. A recent version of PHP 7 and PHPUnit as well as your IDE of choice are all that is needed. No frameworks or third-party code are required. 5 | > 6 | > https://thephp.cc/dates/2019/06/international-php-conference-spring-edition/writing-really-good-code 7 | 8 | Code written for/during the "Writing Really Good Code" workshop at International PHP Conference 2019 (Spring Edition). 9 | 10 | This is example code that is not production-ready. It is intended for studying and learning purposes. 11 | 12 | (c) 2019 thePHP.cc. All rights reserved. 13 | -------------------------------------------------------------------------------- /doc/prices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thePHPcc/ipc19se-writing-really-good-code/dd922e309c0ce66f1b72c1ac75bae4175a6164b8/doc/prices.png -------------------------------------------------------------------------------- /phive.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | 20 | src 21 | 22 | src/autoload.php 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | amount()->amount() * 15 | $this->priceFor($offer->good())->amount() 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Milk.php: -------------------------------------------------------------------------------- 1 | amount = $unit; 19 | $this->good = $good; 20 | } 21 | 22 | public function amount(): Quantity 23 | { 24 | return $this->amount; 25 | } 26 | 27 | public function good(): Good 28 | { 29 | return $this->good; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/OutOfRangeException.php: -------------------------------------------------------------------------------- 1 | amount = $amount; 14 | } 15 | 16 | public function amount(): int 17 | { 18 | return $this->amount; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/PriceList.php: -------------------------------------------------------------------------------- 1 | prices = $prices; 27 | } 28 | 29 | public function current(): Pound 30 | { 31 | return $this->prices[$this->position]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PriceListBuilder.php: -------------------------------------------------------------------------------- 1 | ensureIsGreaterThan0($amount); 14 | 15 | $this->amount = $amount; 16 | } 17 | 18 | public function amount(): int 19 | { 20 | return $this->amount; 21 | } 22 | 23 | private function ensureIsGreaterThan0(int $amount): void 24 | { 25 | if ($amount < 1) { 26 | throw new OutOfRangeException( 27 | \sprintf( 28 | '"%d" is not greater than 0', 29 | $amount 30 | ) 31 | ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 | '/Exception.php', 11 | 'clansofcaledonia\\good' => '/Good.php', 12 | 'clansofcaledonia\\market' => '/Market.php', 13 | 'clansofcaledonia\\milk' => '/Milk.php', 14 | 'clansofcaledonia\\offer' => '/Offer.php', 15 | 'clansofcaledonia\\outofrangeexception' => '/OutOfRangeException.php', 16 | 'clansofcaledonia\\pound' => '/Pound.php', 17 | 'clansofcaledonia\\pricelist' => '/PriceList.php', 18 | 'clansofcaledonia\\pricelistbuilder' => '/PriceListBuilder.php', 19 | 'clansofcaledonia\\quantity' => '/Quantity.php' 20 | ); 21 | } 22 | $cn = strtolower($class); 23 | if (isset($classes[$cn])) { 24 | require __DIR__ . $classes[$cn]; 25 | } 26 | }, 27 | true, 28 | false 29 | ); 30 | // @codeCoverageIgnoreEnd 31 | -------------------------------------------------------------------------------- /tests/GoodTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($milk->isMilk()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/MarketTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(new Pound(5), $market->priceFor(Good::milk())); 21 | } 22 | 23 | public function testMilkCanBeSoldToTheMarket(): Market 24 | { 25 | $market = new Market; 26 | 27 | $payment = $market->sellTo( 28 | new Offer( 29 | new Quantity(2), 30 | Good::milk() 31 | ) 32 | ); 33 | 34 | $this->assertEquals(new Pound(10), $payment); 35 | 36 | return $market; 37 | } 38 | 39 | /** 40 | * @depends testMilkCanBeSoldToTheMarket 41 | */ 42 | public function testSellingMilkToTheMarketReducesMilkPrice(Market $market): void 43 | { 44 | $this->assertEquals(new Pound(4), $market->priceFor(Good::milk())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/OfferTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(new Quantity(1), $offer->amount()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/PoundTest.php: -------------------------------------------------------------------------------- 1 | assertSame($amount, $p->amount()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/PriceListBuilderTest.php: -------------------------------------------------------------------------------- 1 | milkPrices(); 19 | 20 | $this->assertEquals(new Pound(5), $milkPrices->current()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/PriceListTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(new Pound(4), $prices->current()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/QuantityTest.php: -------------------------------------------------------------------------------- 1 | assertSame(1, $unit->amount()); 16 | } 17 | 18 | /** 19 | * @dataProvider invalidAmounts 20 | */ 21 | public function testAmountMustBeGreaterThan0(int $amount): void 22 | { 23 | $this->expectException(OutOfRangeException::class); 24 | 25 | new Quantity($amount); 26 | } 27 | 28 | public function invalidAmounts(): array 29 | { 30 | return [ 31 | [ 32 | 0, 33 | -1, 34 | ], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tools/php-cs-fixer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thePHPcc/ipc19se-writing-really-good-code/dd922e309c0ce66f1b72c1ac75bae4175a6164b8/tools/php-cs-fixer -------------------------------------------------------------------------------- /tools/phpab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thePHPcc/ipc19se-writing-really-good-code/dd922e309c0ce66f1b72c1ac75bae4175a6164b8/tools/phpab -------------------------------------------------------------------------------- /tools/phpunit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thePHPcc/ipc19se-writing-really-good-code/dd922e309c0ce66f1b72c1ac75bae4175a6164b8/tools/phpunit -------------------------------------------------------------------------------- /tools/phpunit.phar: -------------------------------------------------------------------------------- 1 | phpunit -------------------------------------------------------------------------------- /tools/psalm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thePHPcc/ipc19se-writing-really-good-code/dd922e309c0ce66f1b72c1ac75bae4175a6164b8/tools/psalm --------------------------------------------------------------------------------