Yii2 external data retrieving difficulties - php

let me explain my problem, hope you get an idea of what it is.
I have a web service which hide from public access, and have designed a secure way of mysql sql querying using to the service across the websites. so i dont think i can really use the current model layer of Yii2, and that also means i hardly can use activeDataProvider as no database present.
Currently what i do is to write raw sqls and get all the results and then feed into dataprovider using ArrayDataProvider.
e.g.
$sql="select * from a_table";
$result=$remote->select($sql);
$dataProvider = new ArrayDataProvider([
'allModels' => $result,
'sort' => [
'attributes' => ['date', 'name'],
],
'pagination' => [
'pageSize' => 10,
],
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
that pose a problem, everytime i need to query the full table. This is not idea if the table is very large. It is better to query in the size of 10 something, however if i do
$sql="select * from a_table LIMIT 10";
no pagination will appear in my case...How do i solve this problem? And if this is not an idea way to talk to external data services, what is ur suggestion?

The pagination won't appear because you only feed 10 rows to the ArrayDataProvider and it has no way of knowing there is more.
If you want to use DataProvider with remote fetching I would advise you to create your own MyRemoteDataProvider class by extending BaseDataProvider and overriding at least these four methods:
use yii\data\BaseDataProvider;
class MyRemoteDataProvider extends BaseDataProvider
{
protected function prepareModels()
{
}
protected function prepareKeys($models)
{
}
protected function prepareTotalCount()
{
}
protected function sortModels($models, $sort)
{
}
}
.. and then of course use your MyRemoteDataProvider instead of ArrayDataProvider. If you don't know what the methods should return, please read documentation for BaseDataProvider or get inspired by ArrayDataProvider implementation.

Related

Laravel API Resource timestamp "Call to a member function format() on null"

I'm using Laravel's API Resource functionality to format my responses nicely for the client, but the trouble I'm having is with the code below;
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection->transform(function ($item)
{
return [
'id' => $item->id,
'title' => Str::limit($item->title, 32),
'body' => Str::limit($item->body, 32),
'created_at' => $item->created_at->format('d M Y, H:i a'),
'user' => $item->user
];
}),
'links' => [
'current_page' => $this->currentPage(),
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
When using this code, I get an error; "Call to a member function format() on null" on the created_at attribute.
But I've already used dd($this->collection) to confirm that none of the attributes are in fact null and I'm not really sure what could be causing it. My migration contains $table->timestamps();, and inside my factory, I'm not overriding the timestamps at all, so I'm not really sure what the issue is.
Here is the test I'm running below to get this error as well;
factory(News::class, 10)->create();
$user = factory(User::class)->create();
$this->actingAs($user)
->get('/news')
->assertOk()
->assertPropCount('news.data', 10)
->assertPropValue('news.data', function ($news)
{
$this->assertEquals(
[
'id', 'title', 'body', 'created_at',
'user',
],
array_keys($news[0])
);
});
The extra functions such as assertPropCount and assertPropValue are sourced from the InertiaJS demo app as I'm using InertiaJS in my project.
Hopefully, someone is able to help as I've asked around a few other places and no one seems to know what the reason for this is, and based on my debugging there doesn't really seem to be much of a valid explanation as to WHY created_at is null.
As a note, if I turn $item->user to $item->user->toArray() in the code as well, this then also fails complaining that user is null when it isn't. It seems that trying to chain any method onto any attribute causes this null error and I'm not sure why.
First of all keep in mind that the transform function you are using alter the original $this->collection property, you better use map instead that serves the same purpose as transform without altering the original array.
This might be related to your problem because you are modifying the collection you are iterating on, and that can cause issues.
Furthermore, I would suggest you to keep on reading this answer and try out one of the two refactoring alternatives I explained below. That's because I think you are not using API resources correctly and using them properly could actually solve the issue.
Suggestion about your API resource structure
The correct way would be to have two separate files: a News resource and a NewsCollection resource.
This setup allows to define the rendering structure of a single News as well as a collection of News and reuse the former while rendering the latter.
To implement API resources correctly there are a couple of ways (based on what you are trying to achieve):
Note: in both methods, I take for granted that you already have an additional User resource that defines the structure to render a User model (the $this->user property of a News).
1) Create separate classes for single and collection resources
You have to create two files in your resources folder through these two artisan commands:
// Create the single News resource
php artisan make:resource News
// Create the NewsCollection resource
php artisan make:resource NewsCollection
Now you can customize the collection logic:
NewsCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class NewsCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
// Each $this->collection array item will be rendered automatically
// with the News resource definition, so you can leave data as it is
// and just customize the links section/add more data as you wish.
'data' => $this->collection,
'links' => [
'current_page' => $this->currentPage(),
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
}
as well as the single News resource logic:
News.php
<?php
namespace App\Http\Resources;
use App\Http\Resources\User as UserResource;
use Illuminate\Http\Resources\Json\JsonResource;
class News extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => Str::limit($this->title, 32),
'body' => Str::limit($this->body, 32),
'created_at' => $this->created_at->format('d M Y, H:i a'),
'user' => new UserResource($this->user)
];
}
}
To render a news collection, you only have to do:
use App\News;
use App\Http\Resources\NewsCollection;
// ...
return new NewsCollection(News::paginate());
Laravel will automatically reuse the News resource class to render each single element of the NewsCollection's $this->collection array when you are converting the NewsCollection instance for response.
2) Exploit the ::collection method of the single News resource
This method is applicable only if you need metadata about paginated responses (it seems that is what you are trying to achieve with your code).
You just need the single News api resource that you can generate with:
// Create the single News resource
php artisan make:resource News
Then customize the single resource according to your needs:
News.php
<?php
namespace App\Http\Resources;
use App\Http\Resources\User as UserResource;
use Illuminate\Http\Resources\Json\JsonResource;
class News extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => Str::limit($this->title, 32),
'body' => Str::limit($this->body, 32),
'created_at' => $this->created_at->format('d M Y, H:i a'),
'user' => new UserResource($this->user)
];
}
}
Then to render a paginated collection of news, just do:
use App\News;
use App\Http\Resources\News as NewsResource;
// ...
return NewsResource::collection(News::paginate());
The first method allow for a better overall control of the resulting output structure, but I would not structure the $this->collection inside the collection class.
The responsability to define how each collection element should be structured is of the News resource class.
The second method is quicker and works really nice with Laravel pagination allowing you to save quite some time to generate paginated responses with links (that seems what you want to achieve from your code).
Sorry for the long post, if you need further explaination just ask.

Laravel validate data coming from my own application and database

In a function in my controller I call this:
$item = Item::where('i_id', $Id)->where('type', 1)->first();
$firebaseData = app('firebase')->getDatabase()->getReference('items/'.$Id)->getSnapshot()->getValue();
Then I do a lot of "validation" between the data from the two sources above like:
if ($item->time_expires < strtotime(Carbon::now()) && $firebaseData['active'] == 1) {
return response()->json(['errors' => [trans('api.pleaserenew')]], 422);
}
And since this is not data coming from a user/request I cant use Laravels validate method
I dont want to keep this kind of logic inside my controller but where should I put it? Since part of my data is coming from Firebase I cant setup a Eloquent model to handle it either.
I recommend to receive the firebase data via a method within the model:
public function getFirebaseData()
{
app('firebase')->getDatabase()->getReference('items'/ . $this->i_id)->getSnapshot()->getValue();
}
That way you have the logic to receive the data decoupled from controller logic and moved it to where it makes more sense. Adding a validation method could work similarily within the model then:
public function validateData()
{
$combined = array_merge($this->toArray(), $this->getFirebaseData());
Validator::make($combined, [
'active' => 'in:1',
'time_expires' => 'before:' . Carbon::now(),
]);
}
The caveat with this is that the validation error will be thrown within the model instead of the controller, but that shouldn't really be an issue I don't think.
For any data you have in your application you can use Laravel validation.
You can merge your data and process it using Validator facade like this:
$combinedData = array_merge($item->toArray(), $firebaseData);
Validator::make($combinedData, [
'active' => 'required|in:1',
'time_expires' => 'required|before:' . Carbon::now()->toDateTimeString()
], $customMessageArray);
I think the best place for this code is some kind of service class you will inject to controller or other service class using Laravel dependency injection.

Factory Muffin relationships are "null" when using instance method

I'm working on a Laravel 5 app using the jenssegers\laravel-mongodb package and I'm trying to get up and running with Factory Muffin to help with rapidly testing and seeding etc.
I have defined relationships in my factory definitions (code below) - and they work when I run seeds to create new records in the database. By "work", I mean they attach related data correctly to the parent model and persist all those records to the database. But they do not work when I run the $muffin->instance('My\Object') method, which creates a new instance without persisting it. All the relations come back as null.
In a way this makes sense. When the models are stored in the database, they are related by the object _id key. That key doesn't exist until the model is stored. So when I call the instance method, I actually can see via debugging that it does generate the models that would be related, but it does not yet have any key to establish the relation, so that data just kinda goes poof.
This is a bummer because I'd like to be able to generate a fully fleshed-out model with its relations and see for example whether it is saved or passes validation and whatnot when I submit its data to my routes and things. I.e., right now I would not be able to check that the contact's email was valid, etc.
Simplified code samples are below - am I going about this in an entirely wrong way? This is my first time working with Mongo and Factory Muffin. Should this even be able to work the way I want it to?
// factories/all.php
$fm->define('My\Models\Event', [
'title' => Faker::text(75),
'contact' => 'factory|My\Models\Contact'
]);
$fm->define('My\Models\Contact', [
'first_name' => Faker::firstName(),
'email' => Faker::email(),
... etc
]);
// EventControllerTest.php
...
/**
* #var League\FactoryMuffin\FactoryMuffin
*/
protected $muffin;
public function setUp()
{
parent::setUp();
$this->muffin = new FactoryMuffin();
$this->muffin->loadFactories(base_path('tests/factories'));
}
...
public function testCanStoreEvent()
{
$event = $this->muffin->instance('Quirks\Models\Event');
echo $event->contact; // returns null
$event_data = $event->toArray();
$this->call('POST', 'events', $event_data);
$retrieved_event = Event::where('title', '=', $event->title)->get()->first();
$this->assertNotNull($retrieved_event); // passes
$this->assertRedirectedToRoute('events.edit', $retrieved_event->id); // passes
// So, the event is persisted and the controller redirects etc
// BUT, related data is not persisted
}
Ok, in order to get factory muffin to properly flesh out mongo embedded relationships, you have to do like this:
// factories/all.php
$embedded_contact = function() {
$definitions = [
'first_name' => Faker::firstName(),
'email' => Faker::email(),
// ... etc
];
return array_map(
function($item) { return $item(); },
$definitions
);
}
$fm->define('My\Models\Event', [
'title' => Faker::text(75),
'contact' => $embedded_contact
]);
If that seems onerous and hacky ... I agree! I'm definitely interested to hear of easier methods for this. For now I'm proceeding as above and it's working for both testing and seeding.

Laravel 4. Return database table as JSON

Im really new to Laravel. I have manage to set up a database via the migration functionality, and now i want to renturn a table from the database as json. What im working on is kind of a rest-api-thingy. Nothing too fancy.
In my router i have a route going to /api/cases wich inits the controller for the cases. From that controller i basically just want to return a table from my database as JSON.
Router:
Route::resource('/api/cases', 'CasesController');
Controller:
class CasesController extends \BaseController {
public function index()
{
//return db table as json here
}
}
Model:
class Case extends \Eloquent {
protected $fillable = [];
}
And my database looks like this:
I have only one table, named "cases". That one has attributes like "id", "name", "title".
How would i now return that rest-like as json?
You can simply call the toJSON() method:
Case::all()->toJson();
I assume you have your Case model tested and working properly. Once that's done, you can query for all the objects in this table, convert the result to an array, and encode it as JSON.
public function index()
{
return Response::json(Case::all()->toArray());
}
I don't believe it is the job of the ORM to worry about presentation logic, and that is what JSON is. You'll aways need to cast data to various types as well as hide things and sometimes create a buffer zone to rename things safely.
You can do all of that with Fractal which I built for exactly this reason.
<?php namespace App\Transformer;
use Acme\Model\Book;
use League\Fractal\TransformerAbstract;
class BookTransformer extends TransformerAbstract
{
/**
* List of resources possible to include
*
* #var array
*/
protected $availableIncludes = [
'author'
];
/**
* Turn this item object into a generic array
*
* #return array
*/
public function transform(Book $book)
{
return [
'id' => (int) $book->id,
'title' => $book->title,
'year' => (int) $book->yr,
'links' => [
[
'rel' => 'self',
'uri' => '/books/'.$book->id,
]
],
];
}
/**
* Include Author
*
* #return League\Fractal\ItemResource
*/
public function includeAuthor(Book $book)
{
$author = $book->author;
return $this->item($author, new AuthorTransformer);
}
}
Embedding (including) stuff might be a bit more than you need right now, but it can be very handy too.
I often give talks about APIs and the dangers of trying to expose database schema directly. Unless you app is on an internal network, and only your app looks at this data, and your app will never going to change at all then interacting directly with the table is a very bad idea.
Here is my talk, which uses Laravel as an example a few times.

Passing Object Manager in view

Using ZF2 with Doctrine 2.
For the needs of the application i passed objectmanager as a variable ($om) in return new ViewModel
so i can use it in my view:
$om->getRepository('\Schema\Entity\Category')->findBy(.....
The thing that confuses me is:
1) Do I break MVC rules? (Because some say that is a bad practice and some others not)
2) Am I going to face any problems with the development of the application using this tactic?
yes you do
violation of coding standards always has negative side-effects
The Job of the Controller is to pass the Data you need into the View. For this Purpose the Controller interacts with the Model.
public function fooAction()
{
return new ViewModel([
'entities' => $this->getObjectManager()->findBy([
'foo' => 'bar'
])
]);
}
That's pretty much how you will do it. Next question for you should be: What does getObjectManager() do or how do i properly inject the ObjectManager into the Controller?
The answer is as simple: You have to use Controller-Factories
//module.config.php
'controllers' => [
'factories' => [
'YourNamespace\Controller\FooController' => function($cpm) {
$ctrl = new FooController();
$ctrl->setObjectManager(
$cpm->getServiceLocator()->get('Doctrine\ORM\EntityManager')
);
return $ctrl;
}
]
]
And that's all there is to it. Now you have a very clean setup of what you want to achieve ;)

Categories