8

I'm a bit perplexed. I have a simple method on my User (actually "Customer") model to return a user's subscription renewal date:

public function subscriptionRenewalDate() : string
{
    $subscription = $this->subscriptions()->active()->first()->asStripeSubscription();

    return Carbon::createFromTimeStamp($subscription->current_period_end)->format('F jS, Y');
}

I call this method on the authenticated user from a blade template ({{ auth()->user()->subscriptionRenewalDate() }}) and it works fine locally, but as soon as I upload it to the remote staging server it fails with the error:

Cannot declare class App\Models\Customer, because the name is already in use

It points to this line in the Customer model:

class Customer extends Authenticatable
{

What's weird is that it's not just the remote staging server, it's also Travis CI where it fails (when running PHPUnit tests -- these same tests work fine locally).

Obviously it seems like some sort of caching or configuration problem, but I cannot understand what would cause this error.

It's not database related because the tests use RefreshDatabase. If I remove the ENV variable (CASHIER_MODEL: App\Models\Customer) it fails with the expected "cannot find model User" error, so it's correctly getting the ENV variables. I've tried clearing my Laravel caches (php artisan optimize:clear) and composer dump-autoload. It's very confusing.

All I know is that the error is caused by asStripeSubscription(). If I remove that from the method, the blade template loads fine (everything works fine).

To be clear, I can successfully (and this is locally, remotely, and in Travis CI):

  • Register in Laravel
  • Login in Laravel
  • Reset my password
  • Update my personal details
  • Enter payment information in order to subscribe to a Stripe subscription
  • View and modify my Stripe subscription
  • See all my customer and subscription information on Stripe.com
  • Edit my information on Stripe.com and see that updated via the webhook on my servers

The only time there's a problem is when asStripeSubscription() appears in the code. And it's only on remote servers. Locally even that works fine.

I've tried moving this call to various models. I've tried calling it from a controller and passing the result to the view. I've even tried rebooting the server and clearing Travis CI's caches. The error remains the same.

What would cause asStripeSubscription() to generate this error? If I could replicate locally I could debug it! It stubbornly insists on working locally perfectly, but failing remotely.

I'm using Laravel 8, PHP 7.3 and Cashier 12.10.

I just cannot fathom what would lead to the Cannot declare class App\Models\Customer, because the name is already in use error.


Stack trace:

From Laravel.log:

[2021-03-22 10:29:23] production.ERROR: Cannot declare class App\Models\Customer, because the name is already in use {"userId":1127215,"exception":"[object] (Symfony\Component\ErrorHandler\Error\FatalError(code: 0): Cannot declare class App\Models\Customer, because the name is already in use at /var/app/current/app/Models/Customer.php:13) [stacktrace]

enter image description here

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
Chuck Le Butt
  • 47,570
  • 62
  • 203
  • 289
  • did you try `composer dump-autoload`? it may helps – Joseph Mar 19 '21 at 17:36
  • Did you add Billable trait to your user ( customer ) model ? – Kishieel Mar 19 '21 at 17:44
  • @TomaszKisiel Everything relating to Cashier and Stripe work perfectly. I'm able to register, add a subscription, do everything as expected. The only time there's an issue is when `asStripeSubscription()` is used. So yes, the Billable trait is on the Customer model. – Chuck Le Butt Mar 19 '21 at 17:46
  • If you can try to refresh migrations on your server. – Kishieel Mar 19 '21 at 17:52
  • 1
    @TomaszKisiel I've completely wiped my database. Deleted all tables and then run the migrations from scratch. I've then registered and subscribed again, and everything is working fine. I then try to visit a page that calls `asStripeSubscription()` and that also works fine. It's only remotely (hosted server and Travis CI) were it fails. – Chuck Le Butt Mar 19 '21 at 18:48
  • This is a whole stack trace which you get .. nothing else ? – Kishieel Mar 19 '21 at 19:16
  • @TomaszKisiel That's the whole thing. It's bizarre. – Chuck Le Butt Mar 20 '21 at 01:09
  • Did you have same php version on prod and local ? There is other than that app on prod ? – Kishieel Mar 20 '21 at 13:39
  • Can you share your code or only part corelated with stripe somewhere ? I think that can be somthing that we cen't see. Also info about your server can be helpful with reproduction of this error. – Kishieel Mar 20 '21 at 16:53
  • @TomaszKisiel The PHP version is 7.4 on local, and 7.3 on the staging server. I'm currently setting up a new version of the project to see if I can replicate the issue. The servers vary, obviously, but the staging is AWS Elastic Beanstalk (PHP 7.3 running on 64bit Amazon Linux/2.9.15). – Chuck Le Butt Mar 21 '21 at 23:54
  • 1
    Running `php artisan config:cache` may solve the problem. In this case it's indeed a laravel cache creating the conflict. Combine this with `composer dump-autoload -o` (or --optimize) which composer explains as 'Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.' – Dimitri Mostrey Mar 22 '21 at 02:41
  • The `strip` package behavior differs in **Testing** and **Production**. – Abilogos Mar 22 '21 at 08:35
  • @DimitriMostrey You opened the door that led to the solution. I will explain below. Thanks! – Chuck Le Butt Mar 22 '21 at 17:28
  • Try composer dump-autoload It worked for me. – mhh_protto Apr 06 '22 at 11:27

3 Answers3

6

I found the solution. I wasn't aware of this, but it basically was a caching problem relating to ENV variables, and how I was passing those variables in.

With our Elastic Beanstalk staging server, I was declaring environment variables through a .config file in the .ebextensions folder. For example:

option_settings:
  "aws:elasticbeanstalk:application:environment":
     APP_NAME: Membership
     APP_ENV: production
     ...

And this works perfectly well in most cases. You can see the variables in AWS, and most apps will work exactly as expected.

However in this case I needed to make sure I ran php artisan config:cache as part of the deployment (thanks @DimitriMostrey). And if you do this... it ignores the ENV variables you've passed to Elastic Beanstalk! Huh.

I've no idea why, because you can easily confirm that your app is able to read env('DB_HOST') etc. but it won't actually be able to use them to connect to your database or whatever.

And if you don't run config:cache then Cashier doesn't work as expected (giving that utterly confusing error).

So I moved my production environment variables from the .config file to their own .env file, and then used a command to rename that file to .env during deployment.

And now everything works as expected.

With Travis CI it was something similar: I moved important environment variables to .env.travis and then made sure I ran config:cache in .travis.yml.

And in Travis CI now everything works as expected, too.

I had no idea that Elastic Beanstalk environment variables were treated differently to those in .env files... but I won't forget in a hurry!

Chuck Le Butt
  • 47,570
  • 62
  • 203
  • 289
  • 1
    Thanks for sharing the details of how you resolved this! – Nolan H Mar 22 '21 at 19:53
  • Nice you have found the solution. The .env file in production is completely ignored. For security reasons. If you use composer on your production server, I made it a habit to add this line to the `"post-autoload-dump"` object in composer.json: `"@php artisan config:cache --ansi",` Api/route, views and events are also cached, I run these too after a `composer update`. – Dimitri Mostrey Mar 23 '21 at 15:36
  • 1
    .env is used on prod. Once. You will find the values in `bootsrap/cache/config.php`. There are some other files too that may be of interest in the cache map. – Dimitri Mostrey Mar 23 '21 at 15:52
  • @DimitriMostrey Yes, I use a post deploy hook on EB to execute commands relating to caching now. However I believe AWS has removed this hook from Linux 2 based servers, so I will need another solution for caching commands in the future. – Chuck Le Butt Mar 24 '21 at 11:12
  • Seen the answer only later, but my comment was close... with a full stack-trace (available in the error log) it might have been easier to answer than with only the error message. – Martin Zeitler Apr 02 '21 at 22:44
  • @MartinZeitler There was no additional information in the error log (I quoted the only line that made any reference to the issue). And that was the full stack trace in the image. It was a damned frustrating thing to debug! – Chuck Le Butt Apr 04 '21 at 12:19
  • @ChuckLeButt One probably only could produce event-logs with the auto-loader: https://www.php.net/manual/en/language.oop5.autoload.php ...for [example](https://stackoverflow.com/a/12327146/549372). – Martin Zeitler Apr 04 '21 at 16:14
2

PHP may not report duplicate classes, while there are no duplicate classes.

There are generally three possible pitfalls:

A) The issue with that duplicate class may be caused, well, by a duplicate class.
This can be resolved by finding and removing the offending class file, eg:

find . | grep Customer.php

This should return two results, of which one causes the error message; then run rm filename.

B) It might come from an outdated autoload.php, which can be refreshed by running composer dump-autoload.

C) It may be a namespace thing; eg. use Stripe\Customer as StripeCustomer;


That method does nothing but:

StripeSubscription::retrieve(
    ['id' => $this->stripe_id, 'expand' => $expand], $this->owner->stripeOptions()
 );

And App\Models\Customer likely is the $owner of SubscriptionBuilder.php.

Chuck Le Butt
  • 47,570
  • 62
  • 203
  • 289
Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • This was the response from the grep: `./app/Models/Customer.php ./vendor/laravel/cashier/src/Exceptions/InvalidCustomer.php ./vendor/laravel/cashier/src/Concerns/ManagesCustomer.php ./vendor/stripe/stripe-php/lib/Customer.php`. Just a reminder that everything works fine on localhost (where this grep was done). – Chuck Le Butt Mar 20 '21 at 00:40
  • 1
    And this was the result on the EC2 instance where it's failing: `./app/Models/Customer.php ./vendor/laravel/cashier/src/Exceptions/InvalidCustomer.php ./vendor/laravel/cashier/src/Concerns/ManagesCustomer.php ./vendor/stripe/stripe-php/lib/Customer.php` (Identical.) Hmm!!! – Chuck Le Butt Mar 20 '21 at 00:48
  • I renamed Customer to Cust across my app. I updated everything, and then ran all my unit tests. Everything worked perfectly locally. Tested it manually. Worked perfectly. I deployed it and the same issue: `Cannot declare class App\Models\Cust, because the name is already in use`. I ran `find . | grep Cust.php` and only one appeared: `./app/Models/Cust.php`. I am totally baffled. I feel I must have done something stupid somewhere, but I can't figure out where. – Chuck Le Butt Mar 20 '21 at 01:10
  • @ChuckLeButt It is entirely unclear how you deploy the site; temporarily permitting `xdebug` on the remote site might provide some insight. Stuff alike this also can come from `require()` vs. `require_once()`. Try to run `composer dump-autoload` as post-deploy step, maybe? In general, the `.env` might be a whole other - which might be the cause. – Martin Zeitler Apr 02 '21 at 22:24
0

Are you update your relations in database ? Maybe you should refresh your migrations on production server.

If you're using a model other than Laravel's supplied App\Models\User model, you'll need to publish and alter the Cashier migrations provided to match your alternative model's table name.

Another possible problem is that you have not set up a proper model for the auth.

// auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\Customer::class,
    ],
],

Also make sure your model has the correct namespace.

Kishieel
  • 1,811
  • 2
  • 10
  • 19
  • As I said, everything works fine in Travis CI -- so a whole new instance of a machine, with migrations and everything -- until I add `asStripeSubscription()` to the code. As soon as it hits that method, it triggers the error message I mentioned (`Cannot declare class App\Models\Customer, because the name is already in use`). No matter where I put in in the code. And it's only when it hits `asStripeSubscription()`. Everything else still works as expected. – Chuck Le Butt Mar 19 '21 at 17:53
  • can you please share code of this function asStripeSubscription() – Mohit jain Mar 22 '21 at 17:12