{"record":{"id":"e19aca575760c053","repo":"sebastianbergmann/phpunit","slug":"trying-to-double-property-s-of-class-s-with-e19aca","errorCode":null,"errorMessage":"Trying to double property \"%s\" of class \"%s\" with doubleProperties(), but it is not public","messagePattern":"Trying to double property \"(.+?)\" of class \"(.+?)\" with doubleProperties\\(\\), but it is not public","errorType":"exception","errorClass":"PropertyCannotBeDoubledException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/TestDoubleBuilder.php","lineNumber":131,"sourceCode":"            // @codeCoverageIgnoreStart\n        } catch (\\ReflectionException $e) {\n            throw new ReflectionException(\n                $e->getMessage(),\n                $e->getCode(),\n                $e,\n            );\n            // @codeCoverageIgnoreEnd\n        }\n\n        foreach ($properties as $propertyName) {\n            if (!$reflector->hasProperty($propertyName)) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it does not exist');\n            }\n\n            $property = $reflector->getProperty($propertyName);\n\n            if (!$property->isPublic()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is not public');\n            }\n\n            if ($property->isStatic()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is static');\n            }\n\n            if ($property->isReadOnly()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is readonly');\n            }\n\n            if ($property->isFinal()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it is final');\n            }\n\n            if (!$property->hasType()) {\n                throw new PropertyCannotBeDoubledException($this->type, $propertyName, 'it does not declare a type');\n            }\n        }","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/TestDoubleBuilder.php#L113-L149","documentation":"Property doubling works by generating get/set hooks on the mocked class, which is only meaningful for public instance properties — a non-public property's hooks cannot be overridden from the generated subclass. After confirming the property exists, doubleProperties() reflects it and throws PropertyCannotBeDoubledException with reason 'it is not public' when $property->isPublic() is false (TestDoubleBuilder.php:128-132). Protected/private (and PHP 8.1+ readonly, which implies non-public contexts) properties therefore cannot be doubled.","triggerScenarios":"$this->getMockBuilder(Order::class)->doubleProperties(['items']) where Order declares protected array $items or private ?float $total: the property is found by hasProperty() but isPublic() returns false, so the 'it is not public' variant of PropertyCannotBeDoubledException is thrown before any mock code is generated.","commonSituations":"Encapsulation refactors that changed a public property to private/protected while tests still doubled it; entities that expose getters instead of public fields; assuming a promoted constructor property is public when it was promoted as private/private(set); value objects with readonly public properties drifting into the non-public branch via accessor changes.","solutions":["Expose the value through a method instead: mock the getter/setter method (onlyMethods()) rather than doubling the non-public property.","If you control the class and the test really needs hook doubling, widen the property to public (weigh whether that damages the design).","Drop the property from doubleProperties() and assert behavior via the class's public API.","If the property is private on a parent but public on the mocked class itself, double it on the class where it is declared public."],"exampleFix":"// before\nclass Order { protected array $items = []; }\n$mock = $this->getMockBuilder(Order::class)\n             ->doubleProperties(['items']) // 'it is not public'\n             ->getMock();\n\n// after: double the accessor method instead of the protected property\n$mock = $this->getMockBuilder(Order::class)\n             ->onlyMethods(['getItems'])\n             ->getMock();","handlingStrategy":"validation","validationCode":"$reflector = new ReflectionClass(Order::class);\nforeach ($props as $name) {\n    $p = $reflector->getProperty($name);\n    if (!$p->isPublic()) {\n        self::fail(\"\\${$name} is {$p->getVisibility()} — mock the getter instead of doubling it\");\n    }\n}","typeGuard":"/** @return list<non-empty-string> only public, non-static, non-readonly declared props */\nfunction doubleableProperties(string $class): array\n{\n    $r = new ReflectionClass($class);\n    $doubleable = [];\n    foreach ($r->getProperties(ReflectionProperty::IS_PUBLIC) as $p) {\n        if (!$p->isStatic() && !$p->isReadOnly() && !$p->isFinal()) {\n            $doubleable[] = $p->getName();\n        }\n    }\n    return $doubleable;\n}","tryCatchPattern":"use PHPUnit\\Framework\\MockObject\\PropertyCannotBeDoubledException;\n\ntry {\n    $builder->doubleProperties($names);\n} catch (PropertyCannotBeDoubledException $e) {\n    if (str_contains($e->getMessage(), 'not public')) {\n        $builder->onlyMethods($getterNames); // fall back to method mocking\n    } else {\n        throw $e;\n    }\n}","preventionTips":["Default to mocking accessor methods; reserve property doubling for public fields that are part of the public API.","After visibility refactors, re-run property-doubling tests — they are the canary for lost public surface.","Know the full rule set: public, non-static, non-readonly, non-final, typed properties only.","Use promoted constructor promotion carefully: private(set)/readonly promoted props fall into this failure."],"tags":["phpunit","mockobject","property-doubling","visibility","reflection"],"backgroundTag":"mocking-inaccessible-member","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}