array_key_exists() error / edit a vendor file - php

I have a small problem in my laravel API deployed in heroku, that started to happen to me from nowhere, without updating anything or making any relevant changes, and it happens to me when I try to use any eloquent resource, for example when doing:
$brands = Brand::paginate(15);
return BrandResource::collection($brands);
I get this error:
array_key_exists(): Using array_key_exists() on objects is deprecated. Use isset() or property_exists() instead
in DelegatesToResource.php line 49
Investigating a bit, get to the file: DelegatesToResource.php in vendor, and in effect it use:
public function offsetExists($offset)
{
return array_key_exists($offset, $this->resource);
}
To make a test, I created a new Laravel project, and in fact it comes with that line already corrected, like this:
public function offsetExists($offset)
{
return isset($this->resource[$offset]);
}
If there is any way to solve this in my project, I understand that I should not and cannot change files in vendor, so my question is what to do in this case?
I´m using Laravel Framework 5.6.39 and PHP 7.2.18 (cli)

Solution 1
Add updated code to your BrandResource so it may appear like this:
class BrandResource extends JsonResource
{
/**
* Determine if the given attribute exists.
*
* #param mixed $offset
* #return bool
*/
public function offsetExists($offset)
{
return isset($this->resource[$offset]);
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
Solution 2
If you are paginating your data in multiple resources then it's better to extend custom class which contains this updated function rather than extending JsonResource directly.
So it'll look like this:
class CustomResource extends JsonResource
{
/**
* Determine if the given attribute exists.
*
* #param mixed $offset
* #return bool
*/
public function offsetExists($offset)
{
return isset($this->resource[$offset]);
}
}
And Use on your resources like:
class BrandResource extends CustomResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}

Related

Hooks or Events to reduce coupling between packages

What is the best approach to reduce coupling between modules?
For example, if I have an Invoice package and it is related to theCustomer package.
What I understand is that I would have to use a system of "hooks" to inject, for example, a tab with the list of customer invoices in the edit view of a customer.
And in turn, use an event system to know, from the perspective of the "Invoice" package, when, for example, someone tries to delete a client.
What I want to achieve is to reduce the coupling so that if I delete the invoice package, the client package is not affected.
How can I get it? Using Laravel's event system? Using a custom class like the following?
My Hooks class:
class HookRepository
{
/**
* The repository items.
*
* #var \Illuminate\Support\Collection
*/
protected $items;
/**
* Create a new repository instance.
*
* #return void
*/
public function __construct()
{
$this->items = collect();
}
/**
* Dynamically call methods.
*
* #param string $method
* #param array $arguments
* #return mixed
*/
public function __call(string $method, array $arguments)
{
return $this->items->{$method}(...$arguments);
}
/**
* Register a new hook callback.
*
* #param string|array $hook
* #param callable $callback
* #param int $priority
* #return void
*/
public function register($hook, callable $callback, int $priority = 10): void
{
$this->items->push(compact('hook', 'callback', 'priority'));
}
/**
* Apply the callbacks on the given hook and value.
*
* #param string $hook
* #param array $arguments
* #return mixed
*/
public function apply(string $hook, ...$arguments)
{
return $this->items->filter(function ($filter) use ($hook) {
return !! array_filter((array) $filter['hook'], function ($item) use ($hook) {
return Str::is($item, $hook);
});
})->sortBy('priority')->reduce(function ($value, $filter) use ($arguments) {
return call_user_func_array($filter['callback'], [$value] + $arguments);
}, $arguments[0] ?? null);
}
}
Using interfaces and abstract classes to implement Open/Close principle from SOLID
Identify an interface. What ClassA wants from ClassB
Generalize - when you have an interface you will be able to identify common operations that most classes that need to implement it will need it. ( Don't try to future-proof to much here or it will most likely backfire )
Note: If you are doing this in hope you will avoid refactoring your code. Forget it. :) It will only make it a lot easier which already is a huge benefit.
Avoid using hooks to realize Structural and/or Behavioral patterns.
edit>
Neither Package nor Hook is a part of Laravel or Design Patterns lingo.
This itself should give you a hint.
Let me play a guessing game:
<?php
PackageInterface {
public function enable();
public function disable();
public function isEnabled();
public function getHooks(): HookInterface[];
}
HookInterface {
public function injectView();
// or maybe
public function injectIntoView($view);
}
It purely depends on your circumstances when you will load your packages and inject something into view.
You can for example enable() the Package when $invoice->wasPaid() or when Customer enabled the package himself $customer->enable($package)

Laravel nova make resource show only the data of the user

I am trying to do something that seems to go out of the box with how laravel-nova works ...
I have a Batch model/ressource that is used by super admins. Those batch reeports belongs to sevral merchants. We decided to add a layer of connection to are portal and allow merchants to log in and see there data. So obviously, when the merchant visites the batch repport page, he needs to see only data related to it's own account.
So what we did was add the merchant id inside the batch page like this:
nova/resources/batch?mid=0123456789
The problem we then found out is that the get param is not send to the page it self but in a subpage called filter ... so we hacked it and found a way to retreive it like this:
preg_match('/mid\=([0-9]{10})/', $_SERVER['HTTP_REFERER'], $matches);
Now that we have the mid, all we need to do is add a where() to the model but it's not working.
Obviously, this appoach is not the right way ... so my question is not how to make this code work ... but how to approche this to make it so that merchants can only see his own stuff when visiting a controller.
All i really need to is add some sort of a where('external_mid', '=' $mid) and everything is good.
The full code looks like this right now:
<?php
namespace App\Nova;
use App\Nova\Resource;
use Laravel\Nova\Fields\ID;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\Currency;
use Laravel\Nova\Fields\BelongsTo;
use App\Nova\Filters\StatementDate;
use Laravel\Nova\Http\Requests\NovaRequest;
class Batch extends Resource
{
/**
* The model the resource corresponds to.
*
* #var string
*/
//
public static function query(){
preg_match('/mid\=([0-9]{10})/', $_SERVER['HTTP_REFERER'], $matches);
if (isset($matches['1'])&&$matches['1']!=''){
$model = \App\Batch::where('external_mid', '=', $matches['1']);
}else{
$model = \App\Batch::class;
}
return $model;
}
public static $model = $this->query();
/**
* The single value that should be used to represent the resource when being displayed.
*
* #var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* #var array
*/
public static $search = [
'id','customer_name', 'external_mid', 'merchant_id', 'batch_reference', 'customer_batch_reference',
'batch_amt', 'settlement_date', 'fund_amt', 'payment_reference', 'payment_date'
];
/**
* Indicates if the resource should be globally searchable.
*
* #var bool
*/
public static $globallySearchable = false;
/**
* Get the fields displayed by the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function fields(Request $request)
{
return [
ID::make()->hideFromIndex(),
Text::make('Customer','customer_name'),
Text::make('MID','external_mid'),
Text::make('Batch Ref #','batch_reference'),
Text::make('Batch ID','customer_batch_reference'),
Text::make('Batch Date','settlement_date')->sortable(),
Currency::make('Batch Amount','batch_amt'),
Text::make('Funding Reference','payment_reference')->hideFromIndex(),
Text::make('Funding Date','payment_date')->hideFromIndex(),
Currency::make('Funding Amount','fund_amt')->hideFromIndex(),
// **Relationships**
HasMany::make('Transactions'),
BelongsTo::make('Merchant')->hideFromIndex(),
// ***
];
}
/**
* Get the cards available for the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function cards(Request $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function filters(Request $request)
{
return [
];
}
/**
* Get the lenses available for the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function lenses(Request $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function actions(Request $request)
{
return [];
}
}
In Laravel Nova you can modify the result query of any Resource by adding the index Query method. This method allows you to use Eloquent to modify the results with any condition you define.
I understand you just need to maintain the $model property with the model with the default definition and modify the results in the indexQuery method:
...
public static $model = \App\Batch::class;
public static function indexQuery(NovaRequest $request, $query)
{
// Using the same logic of the example above. I recommend to use the $request variable to access data instead of the $_SERVER global variable.
preg_match('/mid\=([0-9]{10})/', $_SERVER['HTTP_REFERER'], $matches);
if (isset($matches['1'])&&$matches['1']!=''){
return $query->where('external_mid', '=', $matches['1']);
}else{
return $query;
}
}
...
About the use of the PHP Global Variable, I recommend you to use the laravel default request() to look into your URL. You can use something like this $request->mid to read the value from the mid value in the URL.

Why does PHPDoc error in PHPStorm over this code?

In the following code the #return is underlined red. I have it expecting an interface to be returned because that is what all of the different Vendor adapters implement.
/**
* VendorFactory constructor.
* #param Model $model
* #return \Traders\Interfaces\VendorAdapterInterface
*/
public function __construct(Model $model)
{
return $this->createAdapter($model);
}
This is the code for the createAdapter which does not have the #return underlined in red.
/**
* #param Model $model
* #return \Traders\Interfaces\VendorAdapterInterface
*/
public function createAdapter(Model $model)
{
$type = str_replace('App\Models\\', '', get_class($model)).'s';
$fqcn = '\Traders\Adapters\\'.$type.'\\'.ucfirst(strtolower($model->name));
return new $fqcn($model);
}
I have tried doing the /** docblock and letting PHPStorm enter what it believes is the return value and it just keeps giving me
#return mixed
Your problem is the return in the constructor.
Constructors do not take return values, they get executed when an instance of that class is instantiated.

Use Laravel 4 Encrypter within a namespace

I am trying to use Illuminate\Encryption\Encrypter; within a namespace which I created. The code really exists in my file.
Problem:
I want laravel to use the key set under 'app/config/app.php' automaticly. However the constructer wants me to set it properly with my hands.
To see how this task is handled by Taylor Otwell, I searched for an example and saw that under "Illuminate\Cache\DatabaseStore", it was set to $encrypter as an instance, using constructer. Tylor also uses a function to getEncrypter(); as follows:
/**
* Get the encrypter instance.
*
* #return \Illuminate\Encryption\Encrypter
*/
public function getEncrypter()
{
return $this->encrypter;
}
Because I want my class to be automaticly loaded with a function in another class; I can't use IoC Container.
Here are my functions and params:
/**
* The encrypter to be used for several reasons.
*/
protected $encrypter = "Illuminate\Encryption\Encrypter";
/**
* Returns the encrypter.
*
* #return Object
*/
public function getEncrypter()
{
return $this->createEncrypter();
}
/**
* Creates a new encrypter object.
*
* #param $string $encrypter
* #return Obj $encrypter instance
*/
public function createEncrypter()
{
$class = '\\'.ltrim($this->encrypter, '\\');
return new $class;
}
/**
* Sets the crypter used by MyFacadeName
*
* #param Encrypter $encrypter
*/
public function setEncrypter($encrypter)
{
$this->encrypter = $encrypter;
}
What is the difference makes the parser think that Taylor is right and I am wrong? What am I missing?
If you are on Laravel and you cannot access the Facades because they have not been loaded/initialized yet:
\Illuminate\Support\Facades\Config::get('app.key');
your last resort might be to access the $app via $GLOBALS:
public function createEncrypter()
{
$class = '\\'.ltrim($this->encrypter, '\\');
return new $class($GLOBALS['app']['config']->get('app.key'));
}

How to use SHA1 encryption instead of BCrypt in Laravel 4?

I'm developing a so called AAC (Automatic Account Creator) for a game, it's basically a site with functions to create accounts, players and several more things for players. The server only supports SHA1 and plain - which is totally unsafe. I can't dive into the source code and make changes. If there's anyway to use SHA1 I would be grateful. I just read about BCrypt, it's great but I can't really change the source code to suit BCrypt. I managed to put SHA1 on registration like this:
$password = $input['password'];
$password = sha1($password);
But I simply can't login. am I doing it wrong? seems like Laravel won't let me login.
I've got get_register and post_register, also I've got get_login and post_login. Do i need to change something in the post_login to make it login or?
any hints?
I'm using Laravel's php server (php artisan serve) and phpMyAdmin on WAMP. I think Laravel checks when you are checking the DB via the Auth::attempt method laravel is doing some form of hashing to check the current pw and the logged in one to check against each other.
You'll have to rewrite the Hash module. Thanks to Laravel's ideas of following IoC and Dependency Injection concepts, it'll be relatively easy.
First, create a app/libraries folder and add it to composer's autoload.classmap:
"autoload": {
"classmap": [
// ...
"app/libraries"
]
},
Now, it's time we create our class. Create a SHAHasher class, implementing Illuminate\Hashing\HasherInterface. We'll need to implement its 3 methods: make, check and needsRehash.
Note: On Laravel 5, implement Illuminate/Contracts/Hashing/Hasher instead of Illuminate\Hashing\HasherInterface.
app/libraries/SHAHasher.php
class SHAHasher implements Illuminate\Hashing\HasherInterface {
/**
* Hash the given value.
*
* #param string $value
* #return array $options
* #return string
*/
public function make($value, array $options = array()) {
return hash('sha1', $value);
}
/**
* Check the given plain value against a hash.
*
* #param string $value
* #param string $hashedValue
* #param array $options
* #return bool
*/
public function check($value, $hashedValue, array $options = array()) {
return $this->make($value) === $hashedValue;
}
/**
* Check if the given hash has been hashed using the given options.
*
* #param string $hashedValue
* #param array $options
* #return bool
*/
public function needsRehash($hashedValue, array $options = array()) {
return false;
}
}
Now that we have our class done, we want it to be used by default, by Laravel. To do so, we'll create SHAHashServiceProvider, extending Illuminate\Support\ServiceProvider, and register it as the hash component:
app/libraries/SHAHashServiceProvider.php
class SHAHashServiceProvider extends Illuminate\Support\ServiceProvider {
/**
* Register the service provider.
*
* #return void
*/
public function register() {
$this->app['hash'] = $this->app->share(function () {
return new SHAHasher();
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides() {
return array('hash');
}
}
Cool, now all we have to do is make sure our app loads the correct service provider. On app/config/app.php, under providers, remove the following line:
'Illuminate\Hashing\HashServiceProvider',
Then, add this one:
'SHAHashServiceProvider',
It took me a lot of time to get something similar happening in Laravel 5.6 but this thread was invaluable. The accepted answer gets you very close but there are still some ambushes along the way (as can be seen in the comments) so instead of struggling with the comments I thought it would be helpful for others to have it presented as an answer.
In my case I needed to access an existing database and couldn't change the user file. The passwords were saved in SHA256 format with a hash key applied as well. So the objective for me was to really only get the check function working.
I'm really new to Laravel and I know there will be a better way around this issue but I couldn't get the app\Libraries area to register so I put both SHAHasher.php and SHAHashServiceProvider.php into app\Providers which I would assume is some sort of Laravel sacrilege but it was the only way I got it to work. :)
The steps I took (hijacking rmobis's excellent answer for Laravel 4) was:
The hash key used in the original app needed to be accessed by Laravel so I added this to the bottom of .env.
.env
...
HASH_KEY=0123_key_code_added_here_xyz
app/Providers/SHAHasher.php
namespace App\Providers;
use Illuminate\Contracts\Hashing\Hasher;
class SHAHasher implements Hasher
{
/**
* Get information about the given hashed value.
* TODO: This was added to stop the abstract method error.
*
* #param string $hashedValue
* #return array
*/
public function info($hashedValue)
{
return password_get_info($hashedValue);
}
/**
* Hash the given value.
*
* #param string $value
* #return array $options
* #return string
*/
public function make($value, array $options = array())
{
// return hash('sha1', $value);
// Add salt and run as SHA256
return hash_hmac('sha256', $value, env('HASH_KEY'));
}
/**
* Check the given plain value against a hash.
*
* #param string $value
* #param string $hashedValue
* #param array $options
* #return bool
*/
public function check($value, $hashedValue, array $options = array())
{
return $this->make($value) === $hashedValue;
}
/**
* Check if the given hash has been hashed using the given options.
*
* #param string $hashedValue
* #param array $options
* #return bool
*/
public function needsRehash($hashedValue, array $options = array())
{
return false;
}
}
app/Providers/SHAHashServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class SHAHashServiceProvider extends ServiceProvider {
/**
* Register the service provider.
*
* #return void
*/
public function register() {
$this->app->singleton('hash', function() {
return new SHAHasher();
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides() {
return array('hash');
}
}
app/config/app.php
remove or comment out // Illuminate\Hashing\HashServiceProvider::class,
Add App\Providers\SHAHashServiceProvider::class,
I didn't need to register the users (only to allow them to use their existing logins to get in) so I only tested it for access. I'm not sure why the app/Libraries area wouldn't take. I was getting an error
Class 'SHAHashServiceProvider' not found
when I ran the composer dump-autoload command until I moved both into app/Providers.
Hope this helps others trying to get the anser to work in Laravel 5.
There is actually a easier (or more simple, at least) solution for a case like this. you can 'fake' the hashing, by using this method in the user model:
public function getAuthPassword() {
return Hash::make($this->password);
}
And hashing the input with your own hash function. For instance, if your passwords are currently hashed with sha1, you can validate the user with
Auth::attempt(array('email' => $email, 'password' => sha1($password))
It doesn't feel like good coding practice, to do it this way, but it will certainly be easier than rewriting the hash module.
The Laravel 7 way
In Laravel 7 adding new hash methods to Laravel got a lot easier you can also still support the old Hash methods instead of basically overwriting them.
First we will create a subfolder in the app folder called Libs or Libaries or basically whatever you want to call it.
In that folder I created a folder called CustomHash where I store all my custom hashers.
Through PSR-4 it is automatically detected and we do not need to add it anywhere.
The Namespace is based on the folder names I chose.
app/AppServiceProvider.php
First we use the namespace of the Sha1Hasher
use App\Libs\CustomHash\Sha1Hasher;
then in the boot() function of your AppServiceProvide.php
Hash::extend("sha1", function($app)
{
return new Sha1Hasher();
});
app/Libs/CustomHash/Sha1Hasher.php
<?php
namespace App\Libs\CustomHash;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Hashing\AbstractHasher;
class Sha1Hasher extends AbstractHasher implements HasherContract {
/**
* Hash the given value.
*
* #param string $value
* #return array $options
* #return string
*/
public function make($value, array $options = array()) {
//I have custom encoding / encryption here//
//Define your custom hashing logic here//
return sha1($value);
}
/**
* Check the given plain value against a hash.
*
* #param string $value
* #param string $hashedValue
* #param array $options
* #return bool
*/
public function check($value, $hashedValue, array $options = array()) {
return $this->make($value) === $hashedValue;
}
/**
* Check if the given hash has been hashed using the given options.
*
* #param string $hashedValue
* #param array $options
* #return bool
*/
public function needsRehash($hashedValue, array $options = array()) {
return false;
}
}
And that's basically it if you want to change the default hash algorithm for everything there is one more thing to do:
config/hashing.php
Change the default driver for hashing to the sha1 driver we implemented with the edits above.
'driver' => 'sha1',
You can do this
dd(
sha1(55),
hash('sha1', 55),
hash_hmac('sha1', 55, env('APP_KEY')),
hash_hmac('sha256', 55, env('APP_KEY')),
hash_hmac('sha512', 55, env('APP_KEY')),
);
// outputs:
/*
"8effee409c625e1a2d8f5033631840e6ce1dcb64"
"8effee409c625e1a2d8f5033631840e6ce1dcb64"
"a2ebb15e6747d1c09f2754787ab390d35c24996e"
"f0fadaa9fdd518947ac3f698196d5370dc2409bbdfbe9e37bd30f935b6cc1f47"
"c1e6f4e144565aa4fdb9c7ae34aba7d43424e20fa40ad3a0641d20bfbb3b9681ded4f4cc8b4661804e4a753118a3f984585d6915ee6d4b75a95310af48afe920"
*/
Laravel 7 UPD
same as Das123 answer but a little fix to
app/Providers/SHAHashServiceProvider.php
namespace App\Libraries\ShaHash;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Hashing\AbstractHasher;
class SHAHasher extends AbstractHasher implements HasherContract
/**
* Register the service provider.
*
* #return void
*/
public function register() {
$this->app->singleton('hash', function() {
return new SHAHasher();
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides() {
return array('hash');
}
}

Categories