├── .php-cs-fixer.php ├── README.md ├── composer.json └── src ├── BlockFrontendAccess.php ├── FrontendAccess.php ├── WithExplicitAccess.php └── WithImplicitAccess.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ['syntax' => 'short'], 8 | 'binary_operator_spaces' => [ 9 | 'default' => 'single_space', 10 | 'operators' => [ 11 | '=>' => null, 12 | '|' => 'no_space', 13 | ], 14 | ], 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'no_superfluous_phpdoc_tags' => true, 18 | 'blank_line_before_statement' => [ 19 | 'statements' => ['return'], 20 | ], 21 | 'braces' => true, 22 | 'cast_spaces' => true, 23 | 'class_definition' => true, 24 | 'concat_space' => [ 25 | 'spacing' => 'one', 26 | ], 27 | 'declare_equal_normalize' => true, 28 | 'elseif' => true, 29 | 'encoding' => true, 30 | 'full_opening_tag' => true, 31 | 'declare_strict_types' => true, 32 | 'fully_qualified_strict_types' => true, // added by Shift 33 | 'function_declaration' => true, 34 | 'function_typehint_space' => true, 35 | 'heredoc_to_nowdoc' => true, 36 | 'include' => true, 37 | 'increment_style' => ['style' => 'post'], 38 | 'indentation_type' => true, 39 | 'linebreak_after_opening_tag' => true, 40 | 'line_ending' => true, 41 | 'lowercase_cast' => true, 42 | 'constant_case' => true, 43 | 'lowercase_keywords' => true, 44 | 'lowercase_static_reference' => true, // added from Symfony 45 | 'magic_method_casing' => true, // added from Symfony 46 | 'magic_constant_casing' => true, 47 | 'method_argument_space' => true, 48 | 'native_function_casing' => true, 49 | 'no_alias_functions' => true, 50 | 'no_extra_blank_lines' => [ 51 | 'tokens' => [ 52 | 'extra', 53 | 'throw', 54 | 'use', 55 | 'use_trait', 56 | ], 57 | ], 58 | 'no_blank_lines_after_class_opening' => true, 59 | 'no_blank_lines_after_phpdoc' => true, 60 | 'no_closing_tag' => true, 61 | 'no_empty_phpdoc' => true, 62 | 'no_empty_statement' => true, 63 | 'no_leading_import_slash' => true, 64 | 'no_leading_namespace_whitespace' => true, 65 | 'no_mixed_echo_print' => [ 66 | 'use' => 'echo', 67 | ], 68 | 'no_multiline_whitespace_around_double_arrow' => true, 69 | 'multiline_whitespace_before_semicolons' => [ 70 | 'strategy' => 'no_multi_line', 71 | ], 72 | 'no_short_bool_cast' => true, 73 | 'no_singleline_whitespace_before_semicolons' => true, 74 | 'no_spaces_after_function_name' => true, 75 | 'no_spaces_around_offset' => true, 76 | 'no_spaces_inside_parenthesis' => true, 77 | 'no_trailing_comma_in_list_call' => true, 78 | 'no_trailing_comma_in_singleline_array' => true, 79 | 'no_trailing_whitespace' => true, 80 | 'no_trailing_whitespace_in_comment' => true, 81 | 'no_unneeded_control_parentheses' => true, 82 | 'no_unreachable_default_argument_value' => true, 83 | 'no_useless_return' => true, 84 | 'no_whitespace_before_comma_in_array' => true, 85 | 'no_whitespace_in_blank_line' => true, 86 | 'normalize_index_brace' => true, 87 | 'not_operator_with_successor_space' => true, 88 | 'object_operator_without_whitespace' => true, 89 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 90 | 'phpdoc_indent' => true, 91 | 'general_phpdoc_tag_rename' => true, 92 | 'phpdoc_no_access' => true, 93 | 'phpdoc_no_package' => true, 94 | 'phpdoc_no_useless_inheritdoc' => true, 95 | 'phpdoc_scalar' => true, 96 | 'phpdoc_single_line_var_spacing' => true, 97 | 'phpdoc_summary' => true, 98 | 'phpdoc_to_comment' => false, 99 | 'phpdoc_trim' => true, 100 | 'phpdoc_types' => true, 101 | 'phpdoc_var_without_name' => true, 102 | 'psr_autoloading' => true, 103 | 'self_accessor' => true, 104 | 'short_scalar_cast' => true, 105 | 'simplified_null_return' => false, // disabled by Shift 106 | 'single_blank_line_at_eof' => true, 107 | 'single_blank_line_before_namespace' => true, 108 | 'single_class_element_per_statement' => true, 109 | 'single_import_per_statement' => false, 110 | 'single_line_after_imports' => true, 111 | 'no_unused_imports' => true, 112 | 'single_line_comment_style' => [ 113 | 'comment_types' => ['hash'], 114 | ], 115 | 'single_quote' => true, 116 | 'space_after_semicolon' => true, 117 | 'standardize_not_equals' => true, 118 | 'switch_case_semicolon_to_colon' => true, 119 | 'switch_case_space' => true, 120 | 'ternary_operator_spaces' => true, 121 | 'trailing_comma_in_multiline' => true, 122 | 'trim_array_spaces' => true, 123 | 'unary_operator_spaces' => true, 124 | 'whitespace_after_comma_in_array' => true, 125 | ]; 126 | 127 | $project_path = getcwd(); 128 | $finder = Finder::create() 129 | ->in([ 130 | $project_path . '/src', 131 | ]) 132 | ->name('*.php') 133 | ->notName('*.blade.php') 134 | ->ignoreDotFiles(true) 135 | ->ignoreVCS(true); 136 | 137 | return (new Config()) 138 | ->setFinder($finder) 139 | ->setRules($rules) 140 | ->setRiskyAllowed(true) 141 | ->setUsingCache(true); 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livewire Access 2 | 3 | This package adds PHP 8.0 attribute support to Livewire. In specific, the attributes are used for flagging component properties and methods as *frontend-accessible*. 4 | 5 | The package ships with two pairs of traits and attributes. One for *explicit* access, and one for *implicit* access. 6 | 7 | ## How it works 8 | 9 | - Components which implement the trait for **explicit** access will *deny* access to all properties and methods if they don't have the `#[FrontendAccess]` attribute. 10 | - Components which implement the trait for **implicit** access will *allow* access to all properties and methods unless they have the `#[BlockFrontendAccess]` attribute. 11 | 12 | This acts as a layer on top of Livewire's `public`-check logic, but gives you much more fine grained control. 13 | 14 | ## Why use this? 15 | 16 | Sometimes, you may want allow access to a component's property in PHP — outside the component — while not allowing access from the frontend. For that, you can use the `WithImplicitAccess` trait. Frontend access will be enabled for all properties by default, but you can disable it for a specific property (or method). 17 | 18 | Other times, you may simply want more assurance than Livewire provides out of the box. The `WithExplicitAccess` trait is made for that. It disables all frontend access, and requires you to manually enable it on specific properties/methods. 19 | 20 | The second option is recommended, because it provides the most security benefits. Accidentally making methods `public` is common, and it can cause security issues. Disabling implicit access can be especially useful on teams with junior engineers who don't yet have a full understanding of Livewire's internals, but can be very productive with it. 21 | 22 | ## Practical use case 23 | 24 | Say you have a component with the following method: 25 | 26 | ```php 27 | public function getItemsProperty() 28 | { 29 | return [ 30 | ['secret' => false, 'name' => 'Item 1'], 31 | ['secret' => true, 'name' => 'Item 2'], 32 | ['secret' => true, 'name' => 'Item 3'], 33 | ['secret' => false, 'name' => 'Item 4'], 34 | ]; 35 | } 36 | ``` 37 | 38 | In the Blade template, you want to loop through the items and only display the non-secret ones. 39 | 40 | ```html 41 | @foreach($this->items->filter(...) as $item) 42 | ``` 43 | 44 | However, the entire dataset will be accessible from the frontend, even if you're not rendering any of the secret items. 45 | 46 | The user can easily fetch the Livewire component in Developer Tools and make a call like this: 47 | 48 | ```js 49 | component.call('getItemsProperty'); 50 | ``` 51 | 52 | It will return all of the data returned by the `getItemsProperty()` method in PHP. 53 | 54 | Screen Shot 2021-03-17 at 21 53 00 55 | 56 | You may think that in this case, you should just make the method `protected`/`private`. However, that would make it inaccessible from the Blade template. Even though Livewire uses `$this` in the template, it's accessing the object from the outside. 57 | 58 | Which means that although Blade templates are completely server-rendered, and let you access any PHP code in a secure way, you cannot access many of the properties or methods of Livewire components without making them public, which can cause unexpected data leaks. 59 | 60 | With this package, you can keep the property public and access it anywhere in PHP, while completely blocking any attempts at accessing it from the frontend. 61 | 62 | ## Installation 63 | 64 | PHP 8 is required. 65 | 66 | ```sh 67 | composer require leanadmin/livewire-access 68 | ``` 69 | 70 | ## Usage 71 | 72 | This package doesn't make any changes to your existing code. Components which don't implement either one of its traits will not be affected. 73 | 74 | ### Explicit access 75 | 76 | To enable the explicit access mode, i.e. only enable access to properties/methods that explicitly allow it, use the `WithExplicitAccess` trait. 77 | 78 | ```php 79 | use Livewire\Component; 80 | use Lean\LivewireAccess\WithExplicitAccess; 81 | use Lean\LivewireAccess\FrontendAccess; 82 | 83 | class MyComponent extends Component 84 | { 85 | // Use the trait on your component to enable this functionality 86 | use WithExplicitAccess; 87 | 88 | // Accessing this from the frontend will throw an exception 89 | public string $inaccessible; 90 | 91 | #[FrontendAccess] 92 | public string $accessible; // This property allows frontend access 93 | 94 | public function secretMethod() 95 | { 96 | // Calling this from the frontend will throw an exception 97 | } 98 | 99 | #[FrontendAccess] 100 | public function publicMethod() 101 | { 102 | // This method allows frontend access 103 | } 104 | } 105 | ``` 106 | 107 | ### Implicit access 108 | 109 | To enable the implicit access mode, i.e. keep using the same mode , use the `WithExplicitAccess` trait. 110 | 111 | ```php 112 | use Livewire\Component; 113 | use Lean\LivewireAccess\WithImplicitAccess; 114 | use Lean\LivewireAccess\BlockFrontendAccess; 115 | 116 | class MyComponent extends Component 117 | { 118 | // Use the trait on your component to enable this functionality 119 | use WithImplicitAccess; 120 | 121 | // This property allows frontend access 122 | public string $accessible; 123 | 124 | #[BlockFrontendAccess] 125 | public string $inaccessible; // This property blocks frontend access 126 | 127 | public function publicMethod() 128 | { 129 | // This method allows frontend access 130 | } 131 | 132 | #[BlockFrontendAccess] 133 | public function secretMethod() 134 | { 135 | // This method blocks frontend access 136 | } 137 | } 138 | ``` 139 | 140 | ### Details 141 | 142 | - The properties still need to be `public` to be accessible. 143 | - The thrown exceptions are identical to those that Livewire would throw if the properties/methods were not public. 144 | 145 | ## Development 146 | 147 | Running all checks locally: 148 | 149 | ```sh 150 | ./check 151 | ``` 152 | 153 | Running tests: 154 | 155 | ```sh 156 | phpunit 157 | ``` 158 | 159 | Code style will be automatically fixed by php-cs-fixer. 160 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leanadmin/livewire-access", 3 | "description": "Control frontend access to properties/methods in Livewire using PHP 8 attributes.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Samuel Štancl", 9 | "email": "samuel.stancl@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Lean\\LivewireAccess\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Lean\\LivewireAccess\\Tests\\": "tests/" 20 | } 21 | }, 22 | "require": { 23 | "php": "^8.0", 24 | "livewire/livewire": "^2.10" 25 | }, 26 | "require-dev": { 27 | "orchestra/testbench": "^7.0|^8.0" 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true 31 | } 32 | -------------------------------------------------------------------------------- /src/BlockFrontendAccess.php: -------------------------------------------------------------------------------- 1 | getTraits()) // Get all traits 17 | ->filter(fn ($reflection, $traitName) => Str::startsWith($traitName, 'Livewire\\')) // Filter those in Livewire namespace 18 | ->map(fn (ReflectionClass $trait) => $trait->getMethods()) // Get their methods 19 | ->map(fn (array $methods) => collect($methods)->map(fn (ReflectionMethod $method) => $method->getName())) // Convert the methods to collections of method names 20 | ->flatten(); // Flatten the collection to get merge the inner collections with method names 21 | 22 | return parent::methodIsPublicAndNotDefinedOnBaseClass($methodName) 23 | && ($livewireMethods->contains($methodName) || (count((new ReflectionMethod($this, $methodName))->getAttributes(FrontendAccess::class)) > 0)); 24 | } 25 | 26 | public function propertyIsPublicAndNotDefinedOnBaseClass($propertyName) 27 | { 28 | $livewireProperties = collect((new ReflectionClass($this))->getTraits()) // Get all traits 29 | ->filter(fn ($reflection, $traitName) => Str::startsWith($traitName, 'Livewire\\')) // Filter those in Livewire namespace 30 | ->map(fn (ReflectionClass $trait) => $trait->getProperties()) // Get their properties 31 | ->map(fn (array $properties) => collect($properties)->map(fn (ReflectionProperty $method) => $method->getName())) // Convert the methods to collections of property names 32 | ->flatten(); // Flatten the collection to get merge the inner collections with property names 33 | 34 | return parent::propertyIsPublicAndNotDefinedOnBaseClass($propertyName) 35 | && ($livewireProperties->contains($propertyName) || (count((new ReflectionProperty($this, $propertyName))->getAttributes(FrontendAccess::class)) > 0)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/WithImplicitAccess.php: -------------------------------------------------------------------------------- 1 | getAttributes(BlockFrontendAccess::class)) === 0; 16 | } 17 | 18 | public function propertyIsPublicAndNotDefinedOnBaseClass($propertyName) 19 | { 20 | return parent::propertyIsPublicAndNotDefinedOnBaseClass($propertyName) 21 | && count((new ReflectionProperty($this, $propertyName))->getAttributes(BlockFrontendAccess::class)) === 0; 22 | } 23 | } 24 | --------------------------------------------------------------------------------