PhpStorm type-hinting for factories? - factory

I have code more or less like this:
class Foo {
public static function factory($str) {
$class = "Foo_" . $str;
return new $class;
}
}
class Foo_Bar {
public function razzle() {
print "Foo_Bar->baz() was called";
}
}
$Obj = Foo::factory('Bar');
and I would like PhpStorm to understand that $Obj is a Foo_Bar object, so that for example if I type $Obj->raz, razzle() will show up for autocompletion.
Is there any way to get this? To tell PhpStorm that the function Foo::factory($str) returns an object of type Foo_$str? My guess is that the answer is no.

As far as I know you can't make PHPStorm understand what Foo is giving out, because it is dynamic. However you can surely tell PHPStorm what your $Obj variable is, by putting a doc comment above it, like so:
/** #var Foo_Bar $Obj */
$Obj = Foo::factory('Bar');

Yes, you can do this if your factory is a static method. It should work fine for the examples you have provided.
More details and some basic example can be found here: http://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata

You should use phpstorm metadata, that works for both dynamic and static calls.
class Foo {
public function factory($str) {
$class = "Foo_" . $str;
return new $class();
}
}
class Foo_Bar {
public function baz() {
print "Foo_Bar->baz() was called";
}
}
$Obj = Foo->factory('Bar');
$Obj->baz();
create a file in your project root directory and name it .phpstorm.meta.php:
<?php
namespace PHPSTORM_META {
$STATIC_METHOD_TYPES = [
\Foo::factory('') => [
"Bar" instanceof \Foo_Bar,
],
];
}
thanks #LazyOne for link.

One way which worked for me is to explicitly list in an "or" list all of the possible classes the factory might spit out.
/**
* #param $str
* #return Foo_Bar | Foo_Cad | Foo_Dad
*/
class Foo {
public static function factory($str) {
$class = "Foo_" . $str;
return new $class;
}
}
But this does mean all the method type hinting on the instance/object will have the union of all of the listed classes.
Not perfect, but still better than nothing, and still better than having to write /* #var ClassA */ multiple times on every instance it occurs.

One way is to explicitly list in an "or" list all of the possible classes the factory might spit out.
<!-- language: lang-php -->
/**
* #param $str
* #return Foo_Bar | Foo_Baz | Foo_Cad
*/
class Foo {
public static function factory($str) {
$class = "Foo_" . $str;
return new $class;
}
}

Related

How to make it easier to access options in Laravel

There is a database structure like this:
users
id
name
user_options
id
user_id
option_name
option_value
There can be a lot of options, so I didn’t make a separate field for each of them.
I would like to make it easier to access an option by its name.
Currently implemented like this:
In the User model:
public function options() : HasMany
{
return $this->hasMany(UserOptions::class);
}
For the test, I write the code directly in web.php in routes:
$user = User::with('options')->first();
$theme = $user
->options
->firstWhere('option_name', '=', 'theme')
->option_value;
There are many options and I would not like to make such a voluminous appeal to each of them.
Please help me to simplify access to options
If you absoltely must access like an object you could add an attribute accessor on your user model like so:
protected function options(): Attribute
{
return Attribute::make(
get: function($_){
if($this->userOptions !== null){ // have the user options not already been set?
$this->setUserOptions() // set them if not
}
return $this->userOptions // return them
}
)
}
private function setUserOptions(): void
{
$this->userOptions = new stdClass()
foreach(UserOptions::where('user_id', $this->id)->get() as $option){
$optionName = $option['option_name']
$optionValue = $option['option_value']
$this->userOptions->$optionName = $optionValue
}
}
Call like
$user->options->theme
But be way of nonexistant options
A much less complex way would be adding a helper function on your user Model though like so:
/**
* #return Collection<UserOption>
*/
public function options(): HasMany
{
return $this->hasMany(UserOption::class, 'user_id', 'id');
}
/**
* #param string $optionName
* #return mixed
*/
public function getOption(string $optionName): mixed
{
/** #var UserOption $option */
foreach($this->options as $option){
if($option->option_name === $optionName){
return $option['option_value'];
}
}
return null;
}
And simply call like $user->getOption('color'); // eg: "red" | null

Laravel 8 vendor class `Illuminate\Database\Eloquent\Factories\Factory` can't resolve name of ModelNameFactory class

Laravel 8 has the default App/Models directory for Model classes. The Illuminate\Database\Eloquent\Factories\Factory has static function resolveFactoryName() to resolve name of ModelNameFactory class
public static function resolveFactoryName(string $modelName)
{
$resolver = static::$factoryNameResolver ?: function (string $modelName) {
$modelName = Str::startsWith($modelName, 'App\\Models\\')
? Str::after($modelName, 'App\\Models\\')
: Str::after($modelName, 'App\\');
return static::$namespace.$modelName.'Factory';
};
return $resolver($modelName);
}
The function works properly only for App/ModelName or App/Models/ModelName
if name of Model class, for example, is the Domain/Customers/Models/ModelName, that function doesn't work properly. What is the best way to fix it?
As you can see here, there is a method called guessFactoryNamesUsing which lets you tell Laravel how it should guess the name of your factories.
Add the following to your AppServiceProvider:
use Illuminate\Database\Eloquent\Factories\Factory;
public function register()
{
Factory::guessFactoryNamesUsing(function ($class) {
return 'Database\\Factories\\' . class_basename($class) . 'Factory';
});
}
Source:
/**
* Specify the callback that should be invoked
* to guess factory names based on dynamic relationship names.
*
* #param callable $callback
* #return void
*/
public static function guessFactoryNamesUsing(callable $callback)
{
static::$factoryNameResolver = $callback;
}
Please put this in your model class in App\Models\ModelName.
Make sure the ModelFactory is the factory name.
protected static function newFactory()
{
return \Modules\Module\Database\Factories\ModelFactory::new();
}

Mocking class instantiation in a feature test

I'm currently working on my first Laravel project — a service endpoint that returns a resource based on a recording saved in S3. The service doesn't require a DB, but my idea was, I could keep the controller skinny, by moving the logic to a "model". I could then access the resource by mimic'ing some standard active record calls.
Functionally, the implementation works as expected, but I am having issues with mocking.
I am using a library to create signed CloudFront URLs, but it is accessed as a static method. When I first started writing my feature test, I found that I was unable to stub the static method. I tried class aliasing with Mockery, but with no luck — I was still hitting the static method. So, I tried wrapping the static method in a little class assuming mocking the class would be easier. Unfortunately, I'm experiencing the same issue. The thing that I am trying to mock is being hit as if I'm not mocking it.
This stack overflow post gives an example of how to use class aliasing, but I can't get it to work.
What is the difference between overload and alias in Mockery?
What am I doing wrong? I'd prefer to get mockery aliasing to work, but instance mocking would be fine. Please point me in the right direction.
 Thank you in advance for your help.
Controller
// app/Http/Controllers/API/V1/RecordingController.php
class RecordingController extends Controller {
public function show($id){
return json_encode(Recording::findOrFail($id));
}
}
Model
// app/Models/Recording.php
namespace App\Models;
use Mockery;
use Carbon\Carbon;
use CloudFrontUrlSigner;
use Storage;
use Illuminate\Support\Arr;
class Recording
{
public $id;
public $url;
private function __construct($array)
{
$this->id = $array['id'];
$this->url = $this->signedURL($array['filename']);
}
// imitates the behavior of the findOrFail function
public static function findOrFail($id): Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)) {
abort(404, "Recording not found with id $id");
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
// imitate the behavior of the find function
public static function find($id): ?Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)){
return null;
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
protected function signedURL($key) : string
{
$url = Storage::url($key);
$signedUrl = new cloudFrontSignedURL($url);
return $signedUrl->getUrl($url);
}
}
/**
* wrapper for static method for testing purposes
*/
class cloudFrontSignedURL {
protected $url;
public function __construct($url) {
$this->url = CloudFrontUrlSigner::sign($url);
}
public function getUrl($url) {
return $this->url;
}
}
Test
// tests/Feature/RecordingsTest.php
namespace Tests\Feature;
use Mockery;
use Faker;
use Tests\TestCase;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithFaker;
/* The following is what my test looked like when I wrapped CloudFrontUrlSigner
* in a class and attempted to mock the class
*/
class RecordingsTest extends TestCase
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL()
{
$recordingMock = \Mockery::mock(Recording::class);
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('getURL')
->once()
->andReturn($returnValue);
$this->app->instance(Recording::class, $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
// The following is what my test looked like when I was trying to alias CloudFrontUrlSigner
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL1()
{
$urlMock = \Mockery::mock('alias:Dreamonkey\cloudFrontSignedURL');
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('sign')
->once()
->andReturn($returnValue);
$this->app->instance('Dreamonkey\cloudFrontSignedURL', $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
phpunit
$ phpunit tests/Feature/RecordingsTest.php --verbose
...
There was 1 failure:
1) Tests\Feature\RecordingsTest::if_a_recording_exists_with_provided_id_it_will_return_a_URL
Expected status code 200 but received 500.
Failed asserting that false is true.
/Users/stevereilly/Projects/media-service/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133
/Users/stevereilly/Projects/media-service/tests/Feature/RecordingsTest.php:85
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:206
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:162
You're getting a 500, which means there's something wrong with the code. Just by scanning it I notice you're missing a filenameFromId method on the Recordings class, and the Test is creating a mock named $recordingMock, but you try to use $urlMock. Try to fix those issues first.
Then you're mocking the class, but you never replace it in your application (you did it in the old test apparently).
Generally you want to follow these steps when mocking:
1. Mock a class
2. Tell Laravel to replace the class with your mock whenever someone requests it
3. Make some assertions against the mock

Laravel - Get value instances via facades

In Illuminate\Support\Facades\Facade abstract In method
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}
static::$app is an instance of Application. And static::$app[$name] that like access value of array, and i don't understand that, What technique here?
ex: static::$app['router'] it return instance of Router. Seem that get values of protected $instances in Illuminate\Container\Container
I think it like example ? but got FATAL ERROR Uncaught Error: Cannot use object of type Foo as array
class Foo
{
public $bar = 'barValue';
}
$foo = new Foo();
echo $foo['bar'];
If you check the API of Illuminate\Container\Container, you will notice that it implements ArrayAccess and consequently the following methods.
offsetExists()
offsetGet()
offsetSet()
offsetUnset()
ArrayAccess lets you access objects as arrays. Here's a very simplistic example of a Container.
<?php
class Container implements ArrayAccess {
private $items = array();
public function __construct() {
$this->items = [
'one' => 1,
'two' => 2,
'three' => 3,
];
}
public function offsetSet($offset, $value) {
if (is_null($offset)) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
public function offsetExists($offset) {
return isset($this->items[$offset]);
}
public function offsetUnset($offset) {
unset($this->items[$offset]);
}
public function offsetGet($offset) {
return isset($this->items[$offset]) ? $this->items[$offset] : null;
}
}
$container = new Container();
echo $container['one']; // outputs 1
$container['four'] = 4; // adds 4 to $items.
echo $container['four']; // outputs 4
As you can see, you can access the Container object as an array since it implements ArrayAccess.
It also doesn't matter if the items property is not publicly accessible. In any case, the implementation of ArrayAccess means that it will allow us to retrieve those values as if they were in an array.

Call method from (custom) controller class in Magento

I'm working with the M2e extension for Magento. Now I want to call a method of the class Ess_M2ePro_Adminhtml_ListingController in the file app/code/community/Ess/M2ePro/controllers/Adminhtml/ListingController.php.
But I don't know, how. I can't create an object or model to get access to the class to use the methods. Maybe it's not a good idea to call this controller methods directly, but in my case (remove a associated magento product to an ebay listing) it's required to call this methods.
In general these actions are called from the magento backend. I've also tried to create an admin_html session, but at the moment I don't have any further ideas.
Here's an example, how it looks like. I'm working with regular PHP-code, nothing special:
class Ess_M2ePro_Adminhtml_ListingController extends Ess_M2ePro_Controller_Adminhtml_MainController
{
//#############################################
protected function _initAction()
{
/** removed **/
}
protected function _isAllowed()
{
return Mage::getSingleton('admin/session')->isAllowed('m2epro/listings/listing');
}
//#############################################
public function indexAction()
{
/** removed **/
}
//#############################################
public function searchAction()
{
/** removed **/
}
public function searchGridAction()
{
/** removed **/
}
public function lockListingNowAction()
{
$listingId = (int)$this->getRequest()->getParam('id');
$component = $this->getRequest()->getParam('component');
$lockItemParams = array(
'id' => $listingId,
'component' => $component
);
$lockItem = Mage::getModel('M2ePro/Listing_LockItem',$lockItemParams);
if (!$lockItem->isExist()) {
$lockItem->create();
}
exit();
}
}
And I'm looking for something like this:
$test = Mage::getModel('M2ePro/Ess_M2ePro_Adminhtml_ListingController')->lockListingNowAction();
You shouldn't call methods from an other controller. Specially in your case, when you have exit at the end of the method.
You can use the _forward method if you are in a controller:
$this->_forward($action = 'lockListingNowAction', $controller = 'adminhtml_listing', $module = 'M2ePro', $params = array('id'=>$id)) //controller name may be different
But the cleanest way is to have the code you need in a helper and call the code from that helper in both controllers.

Resources