Consume External API in Laravel with Guzzle and Save into the Database - laravel

I am using Laravel-5.8 in my server. I have this external endpoint given to me which I cannot alter:
https://api.studklm.net/students/allstudents
It is a GET Request and in JSON (application/json) format.
[
{
"Student Code": "S001",
"Full Name": "Adeleke Omowale",
"StudentClass": "JSS 1",
"AcademicTerm": "First"
},
"Student Code": "S002",
"Full Name": "James Smith",
"StudentClass": "JSS 1",
"AcademicTerm": "First"
},
"Student Code": "S003",
"Full Name": "Haruna Muri",
"StudentClass": JSS 2",
"AcademicTerm": "First"
}
]
Then, in my server I have this table:
CREATE TABLE `students` (
`id` int NOT NULL auto_increment,
`registration_no` varchar(255) UNIQUE NOT NULL,
`full_name` varchar(255) UNIQUE NOT NULL,
`student_class` varchar(255) NOT NULL,
`academic_term` varchar(255) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
I am writing a cron job that will save an external api into my database using Guzzle.
I checked the site below to read some information, but I got stuck along the way.
https://appdividend.com/2018/04/17/laravel-guzzle-http-client-example/
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Student;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp;
use GuzzleHttp\Client;
class studentdataupdate extends Command {
protected $signature = 'command:studentdataupdate';
protected $description = 'Student Data Update';
public function __construct() {
parent::__construct();
}
public function handle() {
$client = new GuzzleHttp\Client();
$res = $client->request('GET','https://api.studklm.net/students/allstudents');
return $result->getBody();
$clients = json_decode($result, true);
foreach($clients as $client) {
Student::updateOrCreate([
'registration_no' => $client->Student Code,
'full_name' => $client->Full Name,
'student_class' => $client->StudentClass,
'facademic_term' => $client->AcademicTerm
]);
}
}
}
I want to achieve these results:
registration_no and full_name are unique, and should not allow duplicates
Some of the fields from the external api are not well formatted, there are spaces in between. For instance, Full Name and Student Code. How do I resolve this while trying to save into my database without error?
If Full Name or Student Code already exists, it updates. Else, it inserts the new record.

There are a couple of easy fixes for your updateOrCreate() method that may help you toward a solution.
First, you can pass strings as attributes in PHP by using curly braces. This comes in handy when there are issues like spaces, or you want to dynamically pass a variable with a string value.
// $client->{$string}
$client->{'Full Name'}
Second, updateOrCreate() accepts not one, but two arrays. The first is used to search for existing entries. The following example would update the database if the registration_no matches an existing entry, or create a new entry if not.
Student::updateOrCreate([
'registration_no' => $client->{'Student Code'},
// 'full_name' => $client->{'Full Name'}, // uncomment to use both
],
[
'full_name' => $client->{'Full Name'}, // not required if uncommented above
'student_class' => $client->StudentClass,
'facademic_term' => $client->AcademicTerm
]);
EDIT:
If you want to do registration_no OR full_name, you may have to search the values manually before updating, as updateOrCreate() uses an AND condition for searching when multiple fields are present.

Create a cron job for Laravel schedule
crontab -e
* * * * * cd /[path-to-your-laravel-project] && php artisan schedule:run >> /dev/null 2>&1
Add your console command to console/kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('command:studentdataupdate')->everyMinute();
}

Related

Making Laravel 9 validation rule that is unique on 2 columns

I am trying to update a row in the pages table.
The slug must be unique in the pages table on the slug and app_id field combined.
i.e. there can be multiple slugs entitled 'this-is-my-slug' but they must have unique app_id.
Therefore I have found that formula for the unique rule is:
unique:table,column,except,idColumn,extraColumn,extraColumnValue
I have an update method and getValidationRules method.
public function update($resource,$id,$request){
$app_id=22;
$request->validate(
$this->getValidationRules($id,$app_id)
);
// ...store
}
When I test for just a unique slug the following works:
public function getValidationRules($id,$app_id){
return [
'title'=> 'required',
'slug'=> 'required|unique:pages,slug,'.$id
];
}
However, when I try and add the app_id into the validation rules it returns server error.
public function getValidationRules($id,$app_id){
return [
'title'=> 'required',
'slug'=> 'required|unique:pages,slug,'.$id.',app_id,'.$app_id
];
}
I have also tried to use the Rule facade, but that also returns server error. Infact I can't even get that working for just the ignore id!
public function getValidationRules($id,$app_id){
return [
'title'=> 'required',
'slug'=> [Rule::unique('pages','slug')->where('app_id',$app_id)->ignore($id)]
];
}
Any help is much appreciated :)
Thanks for the respsonses. It turned out a couple of things were wrong.
Firstly if you want to use the Rule facade for the validation rules, make sure you've included it:
use Illuminate\Validation\Rule;
The other method for defining the validation rule seems to be limited to the following pattern:
unique:table,column,except,idColumn
The blog post that I read that showed you could add additional columns was for laravel 7, so i guess that is no longer the case for laravel 9.
Thanks for your responses and help in the chat!
I recommend you to add your own custom rule.
First run artisan make:rule SlugWithUniqueAppIdRule
This will create new file/class inside App\Rules called SlugWIthUniqueAppRule.php.
Next inside, lets add your custom rule and message when error occured.
public function passes($attribute, $value)
{
// I assume you use model Page for table pages
$app_id = request()->id;
$pageExists = Page::query()
->where('slug', $slug)
->where('app_id', $app_id)
->exists();
return !$pageExists;
}
public function message()
{
return 'The slug must have unique app id.';
}
Than you can use it inside your validation.
return [
'title'=> 'required|string',
'slug' => new SlugWithUniqueAppIdRule(),
];
You can try it again and adjust this custom rule according to your needs.
Bonus:
I recommend to move your form request into separate class.
Run artisan make:request UpdateSlugAppRequest
And check this newly made file in App\Http\Requests.
This request class by default will consists of 2 public methods : authorize() and rules().
Change authorize to return true, or otherwise this route can not be accessed.
Move your rules array from controller into rules().
public function rules()
{
return [
'title'=> 'required|string',
'slug' => new SlugWithUniqueAppIdRule(),
];
}
To use it inside your controller:
public function update(UpdateSlugAppRequest $request, $resource, $id){
// this will return validated inputs in array format
$validated = $request->validated();
// ...store process , move to a ServiceClass
}
This will make your controller a lot slimmer.

Laravel 9 - Set default model attribute to auth user id

I want to pass current user's id into a column by default. I tried giving it in the migration but didn't work, this code did work when I pass an integer but it gives an error when I try to set it to Auth::id()
Code I've tried (in the model file)
protected $attributes = [
'employee_id' => Auth::id(),
];
Error I get :
Constant expression contains invalid operations
It does work when I give it a hard coded string or integer value. But I need to give it the current user's id.
Not sure if it's really a good idea, but you can add this in your Model
protected static function booted()
{
static::creating(function ($model) {
$model->employee_id = Auth::id();
});
}
Check the docs for the complete list of event.
https://laravel.com/docs/9.x/eloquent#events-using-closures

Encrypted Array Won't Save in Eloquent Model

I'm having an odd error with saving an encrypted array in Laravel. The model never updates even when save() is called.
There are no console or SQL errors.
When the encryption is disabled, there are no errors and the model updates successfully.
In a Controller, I'm calling the model like so:
$userData = UserData::where('user_id', $user_id)->first();
I then pull the array:
$encryptedData = $userData->app_data;
And I want to add to this array e.g.
$encryptedData['new'] = 'axy';
$encryptedData['time'] = time();
I then update the model and save it:
$userData->app_data = $encryptedData;
$userData->save();
However, here is where the problem starts. The model does not update. It remains as if nothing happens. Hence if I refresh(), I get the same data as if I had never added the two new entries. When I log it, it looks like this:
Array
(
[token] => xyz
[access_token] => abc
)
After the addition of two new entries:
Array
(
[token] => xyz
[access_token] => abc
[new] => 'axy'
[time] => 1234
)
And after the save() and refresh():
Array
(
[token] => xyz
[access_token] => abc
)
The model looks like this:
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
class UserData extends Model
{
protected $fillable = [
'user_id', 'app_data'
];
protected $casts = [
'user_id' => 'int',
'app_data' => 'array'
];
public function getAppDataAttribute($value)
{
try {
return decrypt($value);
}
catch (DecryptException $e) {
return $value;
}
}
public function setAppDataAttribute($value)
{
$this->attributes['app_data'] = encrypt($value);
}
}
Why are my additions to the array not being saved?
Edit: The strangeness continues
If I call:
UserData::where('id', $userData->id)->update(['app_data' => $encryptedData]);
Then the model does update and does not encrypt, HOWEVER, when I refresh and log the new 'app_data' field, it is returned as a JSON string and not an array as before. I need to cast/decode it to an array each time I want to use it.
Couple of things to look for.
1) The Laravel encrypter uses the app key. Make sure you have one in your .env file. If not, run php artisan key:generate
2) I assume the array is correctly formatted like this:
Array
(
'token' => 'xyz', // You have a = here and no commas after any other value
'access_token' => 'abc'
)
3) Depending on what you are storing this as, you can test by serializing the array before encrypting it:
$arr = serialize($encryptedData); // After you have added new data to the array
$userData->app_data = $arr;
$userData->save();
This is automatic in Laravel, but may give you a help hunting the bug. Test with your mutator using encryptString() and manually unserialize / decryptString() to see if any odd behavior by stepping through the values as they are mutated.

Using Model name from Variable

I have a variable which holds the model name like so
$fooTableName = 'foo_defs';
$fooModel = 'FooDefs';
Now I would like to insert in the DB using that model like so
$fooModel::insert([..foo..array...]);
Throws an error
"message": "Parse error: syntax error, unexpected '$fooModel' (T_VARIABLE), expecting identifier (T_STRING)",
Is it possible to do something like that? or will I be forced to use
DB::table('fooTableName')->insert([...foo...array...]);
If I do it in the latter way, the timestamps in the table are wrong. The created_at column is null and the updated_at has the value
EDIT 1
$model = CustomHelper::getNameSpace($this->tableNames[$i]);
// $model => /var/www/html/erp/app/Models/sales/InvoiceDefs
$model::insert($this->tableCollections[$this->tableNames[$i]]);
Most of them said that, it was namespace issue, so I have corrected it, but still it is throw error like
"message": "Class '/var/www/html/erp/app/Models/sales/InvoiceDefs' not
found",
What you are doing wrong is using model name as string, you need to refactor your code as like below :
$fooModel = 'App\Models\FooDefs';
I have a same situation before and i have created the function to do this
function convertVariableToModelName($modelName='',$nameSpace='')
{
//if the given name space iin array the implode to string with \\
if (is_array($nameSpace))
{
$nameSpace = implode('\\', $nameSpace);
}
//by default laravel ships with name space App so while is $nameSpace is not passed considering the
// model namespace as App
if (empty($nameSpace) || is_null($nameSpace) || $nameSpace === "")
{
$modelNameWithNameSpace = "App".'\\'.$modelName;
}
//if you are using custom name space such as App\Models\Base\Country.php
//$namespce must be ['App','Models','Base']
if (is_array($nameSpace))
{
$modelNameWithNameSpace = $nameSpace.'\\'.$modelName;
}
//if you are passing Such as App in name space
elseif (!is_array($nameSpace) && !empty($nameSpace) && !is_null($nameSpace) && $nameSpace !== "")
{
$modelNameWithNameSpace = $nameSpace.'\\'.$modelName;
}
//if the class exist with current namespace convert to container instance.
if (class_exists($modelNameWithNameSpace))
{
// $currentModelWithNameSpace = Container::getInstance()->make($modelNameWithNameSpace);
// use Illuminate\Container\Container;
$currentModelWithNameSpace = app($modelNameWithNameSpace);
}
//else throw the class not found exception
else
{
throw new \Exception("Unable to find Model : $modelName With NameSpace $nameSpace", E_USER_ERROR);
}
return $currentModelWithNameSpace;
}
How To user it:
Arguments
First Argument => Name of the Model
Second Argument => Namespcce of the Model
For Example we have the model name as Post
$postModel = convertVariableToModelName('Post');
dd($postModel::all());
Will returns all the values in the posts table
But in Some Situation You Model Will in the
Custom Namespace such as App\Models\Admin\User
So this function is created to overcome that
$userModel = convertVariableToModelName('User',['App','Models','Admin']);
dd($userModel::all());
You are feel free to customize the function
Hope it helps
Try the below one,
$fooModel = new FooDefs();
and then you can do the following also,
$fooModel->column1 = $value1;
$fooModel->column2 = $value2;
$fooModel->column2 = $value2;
$fooModel->save();
or
$fooModel->save([
'column1' => $value1,
'column2' => $value2,
'column3' => $value3,
])
Edited answer
$path = 'my\project\path\to\Models';
$fooModel = app($path.'\FooDefs');
$fooModel::save([
'column1' => $value1,
'column2' => $value2,
'column3' => $value3,
])
dd($fooModel ::all());
Try my edited answer.
When you have the name of class stored as string you can call method on that class by using the php method call_user_func_array like this
$class_name = "FooDefs";
call_user_func_array(array($class_name, "insert"), $data);
$data is an Array of data which will be past to the called function as arguments.
Just for simple advice It's will be Good if you save in the $class_name variable the FQN Fully Qualified Name of the class which is the __NAMESPACE__ follow by the name of the class. For sample purpose FQN look like Illuminate\Support\Facades\DB which you can get when you save you use User::class I presume you have some User model. That will return the Fully Qualified Name of the User model will will be App\User in case of Laravel.
$requests = $post['request'] // posting the data from view page
$models = "app\models".'\\'.$requests //geting the model
$model = $models::findOne($referenceId) //fetching value from database

Migration - How to set default value for json? (MySQL)

I have the problem that I need some values to be already set for my settings json column.
Let us say I have this in my user migration file:
$table->json('settings');
My goal is to set let us say these values as default:
'settings' => json_encode([
'mail' => [
'hasNewsletter' => false
],
'time' => [
'timezone' => ''
]
])
How would you do this?
My first approach was to set the values in my UserObserver in the created event after the User was created.
This creates the problem, that my UserFactory is not working correctly. Because a User is created but the settings values are overwritten by the UserObserver again...
Following solution works with Eloquent Model.
For default JSON data you can do something like this in your Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $attributes = [
'settings' => '{
"mail": {
"hasNewsletter" : false
},
"time": {
"timezone" : ""
}
}'
];
}
Then the default value will be {"mail": {"hasNewsletter" : false},"time": {"timezone" :""} in your DB if your input is null. However the existing values in DB will be unchanged and will have to change manually if you need.
If you want to keep your existing DB values null (and/or when null) but want to get as the above default json by Eloquent, you can add the following method in the Model:
protected function castAttribute($key, $value)
{
if ($this->getCastType($key) == 'array' && is_null($value)) {
return '{
"mail":{
"hasNewsletter":false
},
"time":{
"timezone":""
}
}';
}
return parent::castAttribute($key, $value);
}
Note: above castAttribute method will return this same json/data for all null json column of your model. It's better to set empty array here.
Tested in Laravel 5.8.
Reference: https://laravel.com/docs/eloquent#default-attribute-values

Resources