Load model into $this scope - php

I'm doing a learning exercise where I'm building a pretty basic MVC framework. I'm really only doing this to learn more about OOP and it's pros, cons and common pitfalls.
I'm trying to replicate a behaviour, or syntax might be more correct, similar to the very popular framework codeigniter.
That is, I want to manually be able to load a Model from inside my Controller.
Here's how I want to perform it, and subsequently use it.
$this->load->model("mymodel");
$this->mymodel->some_function();
I have my loader working, it tries to run the class and this is how the load->model code looks like
public function model($model)
{
if(file_exists(APPLICATION.'models/'.$model.'.php'))
{
include(APPLICATION.'models/'.$model.'.php');
$this->{$model} = new $model;
}
}
The problem I am having is I get a error running this code saying that the class $model (this should be transformed into mymodel in this case) doesn't exist.
How do I make it so that $model translates into mymodel so the code would perform a action as such: new mymodel;
Thanks for any help, I'm quite the novice in OOP so I might have gotten confused here but really cannot seem to figure this out :/
$this->{$model} does, however, translate into $this->mymodel.
Instead of creating a new thread, I'll add to this.
My next issue has already arisen, since it's basically a followup problem I'll add it here too.
$this->mymodel->some_function() returns the following error;
Notice: Undefined property: Home::$mymodel in C:\xampp\htdocs\application\controllers\home.php on line 16
This error shows when running $this->mymodel->some_function();
Home is the loaded controller.
Hultin

Related

PHPUnit gives error: Target [Illuminate\Contracts\View\Factory] is not instantiable

I created a simple test for my new Laravel 7 application. But when I run php artisan test I get the following error.
Target [Illuminate\Contracts\View\Factory] is not instantiable.
The error doesn't appear when I go to the page in the browser.
$controller = new HomeController();
$request = Request::create('/', 'GET');
$response = $controller->home($request);
$this->assertEquals(200, $response->getStatusCode());
Although "Just write feature tests" may seem like a cop-out ("They're not unit tests!"), it is sound advice if you do not want to get bogged down by framework-specific knowledge.
You see, this is one of those problems that come from using facades, globals, or static methods. All sorts of things happen outside of your code (and thus your test code) in order for things to work.
The problem
To understand what is going on, you first need to know how Laravel utilizes Containers and Factories in order to glue things together.
Next, what happens is:
Your code (in HomeController::home() calls view() somewhere.
view() calls app() to get the factory that creates Views1
app() calls Container::make
Container::make calls Container::resolve1
Container::resolve decides the Factory needs to be built and calls Container::build to do so
Finally Container::build (using PHP's ReflectionClass figures out that \Illuminate\Contracts\View\Factory can not be Instantiated (as it is an interface) and triggers the error you see.
Or, if you're more of a visual thinker:
The reason that the error is triggered is that the framework expects the container to be configured so that a concrete class is known for abstracts (such as interfaces).
The solution
So now we know what is going on, and we want to create a unit-test, what can we do?
One solution might seem to not use view. Just inject the View class yourself! But if you try to do this, you'll quickly find yourself going down a path that will lead to basically recreating loads of framework code in userland. So not such a good idea.
A better solution would be to mock view() (Now it is really a unit!). But that will still require recreating framework code, only, within the test code. Still not that good.[3]
The easiest thing is to simply configure the Container and tell it which class to use. At this point, you could even mock the View class!
Now, purists might complain that this is not "unit" enough, as your tests will still be calling "real" code outside of the code-under-test, but I disagree...
You are using a framework, so use the framework! If your code uses glue provided by the framework, it makes sense for the test to mirror this behavior. As long as you don't call non-glue code, you'll be fine![4]
So, finally, to give you an idea of how this can be done, an example!
The example
Lets say you have a controller that looks a bit like this:
namespace App\Http\Controllers;
class HomeController extends \Illuminate\Routing\Controller
{
public function home()
{
/* ... */
return view('my.view');
}
}
Then your test[5] might look thus:
namespace Tests\Unit\app\Http\Controllers;
use App\Http\Controllers\HomeController;
use Illuminate\Contracts\View\Factory;
class HomeControllerTest extends \PHPUnit\Framework\TestCase
{
public function testHome()
{
/*/ Arange /*/
$mockFactory = $this->createMock(Factory::class);
app()->instance(Factory::class, $mockFactory);
/*/ Assert /*/
$mockFactory->expects(self::once())
->method('make')
->with('my.view')
;
/*/ Act /*/
(new HomeController())->home();
}
}
A more complex example would be to also create a mock View and have that be returned by the mock factory, but I'll leave that as an exercise to the reader.
Footnotes
app() is asked for the interface Illuminate\Contracts\View\Factory, it is not passed a concrete class name
The reason Container::make does nothing other than call another function is that the method name make is defined by PSR-11 and the Laravel container is PSR compliant.
Also, the Feature test logic provided by Laravel already does all of this for you...
Just don't forget to annotate the test with #uses for the glue that is needed, to avoid warnings when PHPUnit is set to strict mode regarding "risky" tests.
Using a variation of the "Arrange, Act, Assert" pattern
This is not how you test endpoints in Laravel. You should let Laravel instantiate the application as it is already setup in the project, the examples you can see here.
What you already wrote can be rewritten to something like this.
$response = $this->call('GET', route('home')); // insert the correct route
$response->assertOk(); // should be 200
For the test to work, you should extend the TestCase.php, that is located in your test folder.
If you're finding this in The Future and you see #Pothcera's wall of text, here's what you need to know:
The ONLY reason he's doing any of that and the ONLY reason you're seeing this in the first place in a Unit test is because he and you haven't changed from PHPUnit\Framework\TestCase to Tests\TestCase in the test file. This exception doesn't exist when you extend the test case that includes app().
My advice would be to simply extend the correct base test case and move on with your life.

use a Helper inside a controller that does not extend AppController

i have created a controller that extends TCPDF to be able to customise a bunch of stuffs ,
also i need to use inside it Helpers .
knowing that i can not have multiple inheritance in php , i tried to create an instance of the view inside the constructor of my new controller to grab the target Helper
like this
class NewPDF extends TCPDF{
public function __construct()
{
$fakeView=new View($this);
$htmlHelper=$fakeView->loadHelper("Html");
# some code ..... parent::__construct()
}
}
it does not work . it gave me weird errors !!!
how can i use a helper inside a controller that does not extend AppController ?
it does not work . it gave me weird errors !!!
It does because you're doing everything totally wrong. That you want to extend a controller with a helper and even throw a view in the mix tells me you have seriously no idea at all how a MVC framework works.
Design patterns in general (MVC is one)
Wikipedia about MVC
CakePHP book explanation of MVC
Random Google article about MVC
Start over here when you understoog the above
At least I'm not going to write in detail what is wrong because like I said, everything is wrong, start with the very basics. The links will explain how to do it right. What you wrote shows a huge lack of knowledge that can't be fixed by a short answer.

Override third party class method inside CakePHP model

I am starting a new project which involves scraping websites and so am planning on using PHPCrawl http://cuab.de/quickstart.html as it looks like the best PHP based solution for this (unless anybody has any other suggestions) but have run into a problem that I can't quite get my head around.
So I import the PHPCrawl class with
App::import('Vendor', 'PHPCrawl', array('file' => 'PHPCrawl/libs/PHPCrawler.class.php'));
Then just underneath that I extend the PHPCrawl class to handle the data like so
class MyCrawler extends PHPCrawler{
function handleDocumentInfo($DocInfo) {
//handle data here
}
}
But my problem comes in when I need to perform CakePHP methods like create() and save() from within that method. I tried creating a new instance of the CakePHP model within the class but that gives me a warning:
Maximum function nesting level of '100' reached, aborting!
So I assume that this is creating an infinite loop of class instances. I guess what I want to do is override the handleDocumentInfo() function but within my CakePHP class, is that possible?
Apologies if this isn't clear, I don't quite know how to go about this one!
For anybody having a similar problem. I got around this by creating a component, importing the class and overriding the method inside that. Had to mess with the original class a little and add a new global var but all seems to be working

Calling function from another controller bug

I have 2 controllers: UsersController and AnalyticsController.
When I run:
//UsersController:
function dummyFunction(){
$this->Analytic->_loadChartFromId($chart_id);
}
the output is:
Query: _loadChartFromId
Warning (512): SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '_loadChartFromId' at line 1 [CORE\cake\libs\model\datasources\dbo_source.php, line 684]
The _loadChartFromId() function takes $chart_id as an argument and returns an array as output. I have no idea why Query: _loadChartFromId appears.
You don't call other controller methods from your controller.
In your users controller, $this->Analytic is an instance of the Analytic model, not the AnalyticsController. So CakePHP thinks you are trying to call a public method called _loadChartFromId() on the Analytic model, which, as you know, doesn't exist.
The reason you get the error is because if you try to call a non-existent method of a model, CakePHP tries to convert it to one of its Magic Find Types. Of course, it's not a valid Magic Find Type either, so you get a SQL error.
Solution
It's difficult to provide a complete solution as we only have part of your code, but you are perhaps violating the concept of MVC with the way you're coding your app.
You need to do one of two things:
Move _loadChartFromId() to your users controller. This seems to me like it would be counter-intuitive, as it probably has nothing to do with the User.
Move the method to your Analytic model. You would need to make it public so the controller can access it, and in your users controller you would need to make sure you have the Analytic model loaded.
class Analytic extends AppModel {
public function _loadChartFromId($chart_id) {
// ...
}
}
Then you can call the method as you were doing before, from your users controller.
I could have opted to close this question as an exact duplicate of at least 5 other questions (if you search for "cakephp another controller").
But the answers there are just terrible. They actually try to invoke new Dispatchers or requestAction().
So if your question is about another controller method:
The short answer is: You don't.
The long answer: You still dont. That's a typical beginners mistake.
You should put the functionality into a component if it is mainly business logic. The component then can be accessed from multiple controllers.
If it is more like model data (as in your example), put the functionality into the model layer (in an appropriate model). this way you can also access it from anywhere in your application.
Also: Accessing protected methods from other objects is never a good idea. Use public methods if you intend to use it from "outside" the object.
If your question is about a model method:
You need to include your model in your controller before you can use it.
Either by using public $uses or by using loadModel('ModelName') or even ClassRegistry::init('ModelName').

Replicating Codeigniters load function

I have recently been playing around with Codeigniter to see what I can learn from it. I came across the load function and was wondering if anyone knows how its done. Basically, it looks something like:
$this->load->model('Model_name');
$this->Model_name->some_function();
Now load is obviously a class and an instance is created and called load. And load includes the class "Model_name" and creates an instance of it. But the part I cant work out, is how does the load class create a "class variable" named "Model_name" to be used as in the second line of the code? And how would I actually go about implementing this in php.
Thanks.
What the class basicly does is remembering all the created objects ($this for instance) and then assign the newly created class by reference as a variable in those classes.
function Load($className)
{
$newClass = new $className();
foreach($this->objects as &$object) //objects is array with created objects
$object->$className = $newClass;
}
however, it does a lot more stuff in the background than that. You know you can just open 'loader.php' and then read what it does, right?
This kind of things work with interpreted languages like PHP. Though it can be very confusing to picture this, specially if you are experienced with strict languages such as C++, C# etc.
The idea is, there are PHP functions that can execute PHP code and the result will be visible elsewhere in the script.

Categories