phpspec / prophecy

Highly opinionated mocking framework for PHP 5.3+
MIT License
8.53k stars 241 forks source link

PHP Syntax error out of generated class #209

Open walterdolce opened 9 years ago

walterdolce commented 9 years ago

While working on Magento2 and trying to spec one of the classes I was working on I stumbled into this error caused by the code generation Prophecy performs.

I'm not sure if/how that's reproducible without using Magento2, so I'll assume that you have a working instance to reproduce it in this case.

The specific error is: PHP Parse error: syntax error, unexpected '(', expecting identifier (T_STRING) in /vagrant/vendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCreator.php(49) : eval()'d code on line 286

Here's the spec:

<?php

namespace spec\VendorName\Newsletter\Model;

use Magento\Framework\Model\Context;
use Magento\Framework\Registry;
use Magento\Newsletter\Model\Subscriber as MagentoSubscriber;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class SubscriberSpec extends ObjectBehavior
{
    function it_is_a_magento_model(MagentoSubscriber $subscriber, Context $context, Registry $registry)
    {
        $this->beConstructedWith($subscriber, $context, $registry);
        $this->shouldHaveType('Magento\Framework\Model\AbstractModel');
    }
}

Here's the actual class:

<?php
namespace VendorName\Newsletter\Model;

use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\Resource\AbstractResource;
use Magento\Framework\Registry;
use Magento\Newsletter\Model\Subscriber as MagentoSubscriber;

class Subscriber extends AbstractModel
{
    /**
     * @var MagentoSubscriber $magentoSubscriber
     */
    protected $magentoSubscriber;

    /**
     * @param MagentoSubscriber     $magentoSubscriber
     * @param Context               $context
     * @param Registry              $registry
     * @param AbstractResource|null $resource
     * @param AbstractDb|null       $resourceCollection
     * @param array                 $data
     */
    public function __construct(
        MagentoSubscriber $magentoSubscriber,
        Context $context,
        Registry $registry,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        parent::__construct($context, $registry, $resource, $resourceCollection, $data);
        $this->magentoSubscriber = $magentoSubscriber;
    }

    protected function _construct()
    {
        $this->_init('VendorName\Newsletter\Model\Resource\Subscriber');
    }
}

Here's the generated code. I think it's missing the closing curly bracket (}) for the namespace. Please note that even by hardcoding it and ensure code correctness, the class will not be doubled anyway.

<?php

namespace Double\Magento\Newsletter\Model\Subscriber {
    class P1 extends \Magento\Newsletter\Model\Subscriber
        implements \Prophecy\Prophecy\ProphecySubjectInterface, \Prophecy\Doubler\Generator\ReflectionInterface
    {
        private $objectProphecy;

        public function __construct(\Magento\Framework\Model\Context $context = NULL, \Magento\Framework\Registry $registry = NULL, \Magento\Newsletter\Helper\Data $newsletterData = NULL, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = NULL, \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder = NULL, \Magento\Store\Model\StoreManagerInterface $storeManager = NULL, \Magento\Customer\Model\Session $customerSession = NULL, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository = NULL, \Magento\Customer\Api\AccountManagementInterface $customerAccountManagement = NULL, \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation = NULL, \Magento\Framework\Model\Resource\AbstractResource $resource = NULL, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = NULL, array $data = NULL)
        {
            if (0 < func_num_args()) {
                call_user_func_array(array('parent', '__construct'), func_get_args());
            }
        }

        public function getId()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setId($value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getCode()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getConfirmationLink()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getUnsubscriptionLink()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setCode($value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getStatus()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setStatus($value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setMessagesScope($scope)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getEmail()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setEmail($value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setStatusChanged($value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function isStatusChanged()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function isSubscribed()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function loadByEmail($subscriberEmail)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function loadByCustomerId($customerId)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function randomSequence($length = 32)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function subscribe($email)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function unsubscribe()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function subscribeCustomerById($customerId)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function unsubscribeCustomerById($customerId)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function updateSubscription($customerId)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function confirm($code)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function received(\Magento\Newsletter\Model\Queue $queue)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function sendConfirmationRequestEmail()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function sendConfirmationSuccessEmail()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function sendUnsubscriptionEmail()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getSubscriberFullName()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function __sleep()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function __wakeup()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setIdFieldName($name)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getIdFieldName()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function isDeleted($isDeleted = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function hasDataChanges()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setData($key, $value = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function unsetData($key = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setDataChanges($value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getOrigData($key = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setOrigData($key = NULL, $data = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function dataHasChangedFor($field)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getResourceName()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getResourceCollection()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getCollection()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function load($modelId, $field = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function afterLoad()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function isSaveAllowed()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setHasDataChanges($flag)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function save()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function afterCommitCallback()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function isObjectNew($flag = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function beforeSave()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function validateBeforeSave()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getCacheTags()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function cleanModelCache()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function afterSave()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function delete()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function beforeDelete()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function afterDelete()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function afterDeleteCommit()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getResource()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getEntityId()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setEntityId($entityId)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function clearInstance()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getStoredData()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getEventPrefix()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function addData(array $arr)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getData($key = '', $index = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getDataByPath($path)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getDataByKey($key)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setDataUsingMethod($key, $args = array())
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getDataUsingMethod($key, $args = NULL)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function hasData($key = '')
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function toArray(array $keys = array())
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function convertToArray(array $keys = array())
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function toXml(array $keys = array(), $rootName = 'item', $addOpenTag = false, $addCdata = true)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function convertToXml(array $arrAttributes = array(), $rootName = 'item', $addOpenTag = false, $addCdata = true)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function toJson(array $keys = array())
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function convertToJson(array $keys = array())
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function toString($format = '')
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function __call($method, $args)
        {
            throw new \Prophecy\Exception\Doubler\MethodNotFoundException(sprintf('Method `%s::%s()` not found.', get_class($this), func_get_arg(0)), $this->getProphecy(), func_get_arg(0));
        }

        public function isEmpty()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function serialize($keys = array(), $valueSeparator = '=', $fieldSeparator = ' ', $quote = '"')
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function debug($data = NULL, &$objects = array())
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function offsetSet($offset, $value)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function offsetExists($offset)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function offsetUnset($offset)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function offsetGet($offset)
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getStoreId()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function ()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getChangeStatusAt()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getCustomerId()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getSubscriberEmail()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getSubscriberStatus()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getSubscriberConfirmCode()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function getSubscriberId()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setSubscriberId()
        {
            return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());
        }

        public function setProphecy(\Prophecy\Prophecy\ProphecyInterface $prophecy)
        {
            $this->objectProphecy = $prophecy;
        }

        public function getProphecy()
        {
            return $this->objectProphecy;
        }
    }

Please let me know if you need anything else.

stof commented 9 years ago

There is a function generated without a name here. This looks really weird.

walterdolce commented 9 years ago

Yep @stof , sorry didn't notice that on the first look. That'd be an other weird one yes. Not sure why is like that.

Magento makes use of __call where it intercepts "get"s and "set"s so that you can do setSomething("foo") which would end up being used by an internal setData('something', 'foo'), if that's of any help. Same applies for get(s): getSomething() is sent to the internal getData('something')

walterdolce commented 9 years ago

For reference, this is where that's happening (assuming the class is of that type): https://github.com/magento/magento2/blob/develop/lib/internal/Magento/Framework/DataObject.php#L378

stof commented 9 years ago

Well, magento sets the @method phpdoc for its magi methods, so they should be understood by Prophecy. The place where the method without name appears makes me think it is related to magic methods (it is in the middle of other magic methods). The phpdoc for magic methods looks right in the Magento source code in the develop branch. Can you tell me which version of Magento you are using exactly ? Maybe they fixed their phpdoc.

walterdolce commented 9 years ago

Currently using 1.0.0-beta3

stof commented 9 years ago

there is no such tag in the repository. This will make it harder...

Can you paste the phpdoc of the Magento\Newsletter\Model\Subscriber class available in your version ? I only need the class-level phpdoc.

walterdolce commented 9 years ago

:-/

Sure. This is it:

/**
 * Subscriber model
 *
 * @method \Magento\Newsletter\Model\Resource\Subscriber _getResource()
 * @method \Magento\Newsletter\Model\Resource\Subscriber getResource()
 * @method int getStoreId()
 * @method $this setStoreId(int $value)
 * @method string getChangeStatusAt()
 * @method $this setChangeStatusAt(string $value)
 * @method int getCustomerId()
 * @method $this setCustomerId(int $value)
 * @method string getSubscriberEmail()
 * @method $this setSubscriberEmail(string $value)
 * @method int getSubscriberStatus()
 * @method $this setSubscriberStatus(int $value)
 * @method string getSubscriberConfirmCode()
 * @method $this setSubscriberConfirmCode(string $value)
 * @method int getSubscriberId()
 * @method Subscriber setSubscriberId(int $value)
 *
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 */
tkotosz commented 9 years ago

It seems it is invalid @method again. If you replace the all $this in the docblock with \Magento\Newsletter\Model\Subscriber then prophecy will be able to create the double. So the docblock should be something like this:

/**
 * Subscriber model
 *
 * @method \Magento\Newsletter\Model\Resource\Subscriber _getResource()
 * @method \Magento\Newsletter\Model\Resource\Subscriber getResource()
 * @method int getStoreId()
 * @method \Magento\Newsletter\Model\Subscriber setStoreId(int $value)
 * @method string getChangeStatusAt()
 * @method \Magento\Newsletter\Model\Subscriber setChangeStatusAt(string $value)
 * @method int getCustomerId()
 * @method \Magento\Newsletter\Model\Subscriber setCustomerId(int $value)
 * @method string getSubscriberEmail()
 * @method \Magento\Newsletter\Model\Subscriber setSubscriberEmail(string $value)
 * @method int getSubscriberStatus()
 * @method \Magento\Newsletter\Model\Subscriber setSubscriberStatus(int $value)
 * @method string getSubscriberConfirmCode()
 * @method \Magento\Newsletter\Model\Subscriber setSubscriberConfirmCode(string $value)
 * @method int getSubscriberId()
 * @method Subscriber setSubscriberId(int $value)
 *
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 */

@walterdolce if this change fix your problem then I will include this fix in this PR: https://github.com/magento/magento2/pull/1792

stof commented 9 years ago

Well, we should support $this then.

ciaranmcnulty commented 9 years ago

Is it an error on Magento's part that we should support, or is it valid syntax?

ciaranmcnulty commented 9 years ago

In the broader picture it would be good to handle the eval failing in a cleaner way, if that's even possible.

stof commented 9 years ago

which eval ?

and $this is valid in the PSR-5 draft to document fluid APIs IIRC

ciaranmcnulty commented 9 years ago

The Prophecy double-generating eval that's erroring. It would be good to transform it into a catchable exception.

nagno commented 8 years ago

Hey guys,

What do you think about $this? Are you going to support it in a future version?

aik099 commented 8 years ago

Why not. Just send a PR.

stof commented 8 years ago

should be fixed in 1.6.1