Project Setup and Guzzle0:00
So today we're gonna work with the news API. This API is a REST API, so we just returned some articles for us, gathered from the web. So I started a new project from scratch. It's just an empty Laravel project, php, artisan, serve. It's just an empty project, so there's nothing else in here than what you normally see in your Laravel application. So if we open this up, we can see it's an empty Laravel project. And we need to install Guzzle. So composer require guzzlehttp/guzzle. So Guzzle is an HTTP library, which we can use to do HTTP requests to other websites. And it's mostly used for APIs, I guess. And it's really powerful because we can do really cool stuff with it. We can init stuff into that, and so on. So back to PHPStorm. We now added our
Controller and Routing0:40
because we can do really cool stuff with it. We can init stuff into that, and so on. So back to PHPStorm. We now added our pendency in here, so we now have Guzzle in here. So from here we can keep working on it. Let's start by making our controller. So php artisan make:controller NewsController. And I want an invocable controller as well. So if I'm going back here, we now have a NewsController, which is rather empty. Let's first upgrade the route that belongs to this controller. So if I'm going to web.php, in here we can say Route::get('news'), and we're gonna connect it to the NewsController::class. So if I'm going to the route search provider, you can see in here that we include the namespace here. Because we do this, we
to the NewsController. So if I'm going to the route searchProvider, you can see in here that we include the namespace here. Because we do this, we can't really use the namespace here because it's gonna load it twice. So if I open up in a browser and go to news, we get an error here. See, it can't find the class anymore. So instead I just removed this here. We don't really need the namespace here. So if I now refresh, we get an empty controller. So in here if I return news as a string, we get news back. So we're all good. So let's think out the API we want to use. So news is something that can be used often in applications. So you might have a sidebar where you display some news, or you have another section where you display news. Let's create a service class which we can
Building NewsService2:02
applications. So you might have a sidebar where you display some news, or you have another section where you display news. Let's create a service class which we can reuse. So let's call it NewsService. In this case we want to get the headlines. So in this case we're gonna do NewsService::headlines. So this is the data that I want to work with. And in this case let's just dump it here so we can see it later here, what this returns. Well this class doesn't exist yet, so let's create this class. Let's call it Services. It's a nice place for our classes. And we call it NewsService. So this is just an empty class and we need to add one method, which is the headlines method. So what I want to do is to return a collection in here. We want to make sure that we always
class and we need to add one method, which is the headlines method. So what I want to do is to return a Collection in here. We want to make sure that we always get a Collection. So from here we have to think how we're gonna implement this. So we need to do a few steps. Retrieve data from using Guzzle. So retrieve the data from the API. Convert the data to a Collection, because we like collections and it's easy to work with. And then finally we return the Collection in here. So now we have Guzzle, we can just start doing this really easily. So let's say we need a Guzzle client. So Guzzle does the HTTP request, right? So we need just a simple client that can do this for us. So new Client(). And from here we can just say, so the response will be $client->get and then some specific URL. So if we go
simple client that can do this for us. So new Client. And from here we can just say, so the response will be clients get and then some specific URL. So if we go back to the documentation of this thing, we have this headlines endpoint. So this headlines endpoint shows us how we can do the request to the API. And it will give us a response that looks like this. So it has a status, totalResults and an articles array. So let's just copy over the URL, because that's the one we're going to use for our API. So if we now die the response in here and we go back to our browser and refresh, we can see that it doesn't work. Oh yeah, of course. We didn't import the service yet. So let's import the service and let's continue. So we're going back here. So if I'm going now to /news, we get the response.
Parsing API Response4:04
We didn't import the service yet. So let's import the service and let's continue. So we're going back here. So if I'm going now to /news, we get the response back. And as you can see, this is just a JSON response or a response object from Guzzle rather. And there's no data here. There's a stream which holds all the data. There are some extra headers that we get back, but there's no data here. We don't can really see it. So we need to tell Guzzle that it needs to fetch the data for us or at least return it to turn it into a response that we can use. So in this case, we can say our the body that we get back here is response->getBody(). Now this is still a string, so we need to make sure that this is actually a string. We know that the API returns JSON, so we need to convert it to an array.
body. Now this is still a string, so we need to make sure that this is actually a string. We know that the API returns JSON, so we need to convert it to an array that we can work with. So we can use json_decode here. And we can pass in the second argument to make it an associative array, which basically means is that we get a key value based array back instead of PHP objects. Now that we have our body, let's just dump the body so you can see what we get back here. So now we still get the same setup, right? Like the API showed us, status, total_results, articles. And from here we get just 28 results, and there's all the data we need. So basically they're all the same, so we get a huge list of data. So let's convert the data now to some object. So let's convert it here. Let's
we need. So basically they're all the same, so we get a huge list of data. So let's convert the data now to some object. So let's convert it here. Let's make a Collection out of the body, and then we just map it into some articles. So we get an Article object back. So this is the Collection we're working with, which we will return here at the end. So here we return the data that we get back. So as we can see in here, we have an author. So let's just use the author, article, author. And we're not going to return all fields, but at least some fields. And we have the title in here. We have the URL. Let's just use the URL as well. Let's keep it to URL, Image URL, and I believe they, I just don't find the name URL to image a normal name, so I just use
We have the URL. Let's just use the URL as well. Let's keep it to URL. Image URL, and I believe they, I just don't find the name URL to image a normal name, so I just use image URL. And then finally we have publishedAd. And as you can see here, we can use publishedAd here. However, this is a different format, and we can't really parse that. So let's parse that to a Carbon object. However, the publishedAd doesn't always need to have a date. So we need to make sure that this actually is a date. Let's just do something like this. So if it's not equal to null, we're going to parse it, and otherwise we're gonna say it's unknown. Just need to make sure that this is not empty. And then we can parse here. So we can say Carbon::createFromFormat, because we already know the format, right? So we
to make sure that this is not empty. And then we can parse here. So we can say Carbon::createFromFormat, because we already know the format, right? So we have to specify the format here, and then the data in here. So like this. So let's clean this up a little bit, and then we return. Okay, we have this T in here, and we have this Z in here, which is basically the time zone stuff. But we can work around that. So we have year, month, date. Then we have the big T. Then the time, and then the big Z. So this should actually work for us. Let's clean it up a little bit more. Okay, now we convert this to a collection. Now let's see what we get back when we do this. So if I refresh now, we should get back. Okay, we get an error here. Article. Let's see what article is for us.
see what we get back when we do this. So if I refresh now, we should get back. Okay, we get an error here. Article. Let's see what article is for us. Okay, so it's okay. Yeah, it makes sense, right? We're looping over the body, but like we said before, the body exists out of multiple responses, or multiple keys here. So we need to be specific here about articles. So instead of body, we have to say articles. So if we refresh now, we get back a collection with all the items we specified. And we'll get this Carbon object right, which we can understand when we can work with. So that's really nice. Okay, so that's it for the collection parts. If we refresh now, we still get this dd back because we used that in here, dd. So if we go back to our Service class here, we can see
the collection parts. If we refresh now, we still get this DD back because we used that in here, DD. So if we go back to our Service class here, we can see here that we have hard-coded a few stuff in here. So this all works perfectly, and we can use this for our headlines. But what happens if we have another Article with an archive or something? We want to get all the archived Articles in our Service class. How are we going to handle that? Because we just don't want to copy all of this over every time. So instead, we can probably move a lot of this stuff to somewhere else. So for example, the URL. We can put that in Guzzle, because Guzzle is, in this case, the owner of the client. So we don't really care what URL we're talking about. We just want to make sure that we get the top headlines
Custom Client and Provider8:57
Guzzle is, in this case, the owner of the client. So we don't really care what URL we're talking about. We just want to make sure that we get the top headlines from a specific URL. And the same goes for the API key. We don't really want to put that in our service class, right? That's authentication. We don't want to handle that somewhere else. Let's see if we can move this out. We have this constructor here. So let's create a constructor. We're going to inject the client here. So we're going to create a new client called NewsClient. Oh, we can just call this client. That's fine. We initialize the field, and this can be private. So this class doesn't exist yet, but we can replace this with doing this instead. We can replace it with this client now. So we already removed this part.
private. So this class doesn't exist yet, but we can replace this with doing this instead. We can replace it with this client now. So we already removed this part. Let's create this class. So if I'm here, HTTP, we create a new directory called clients, because we're connecting to some HTTP client, and we call this NewsClient. And this class extends the Client from Guzzle, which helps us give us all the same functionality. So you might think, why do we create this class that just extends Client? Why not just use Client? Well, we want to register this later in the container, so we can add extra functionality to it. And then we need a unique name for it, or at least a unique identifier. And it's a really good pattern to just use a class, and use the class name as the name in the container,
unique name for it, or at least a unique identifier. And it's a really good pattern to just use a class, and use the class name as the name in the container, which is much easier to specify in your code as well. So we can now import this class. So we have this client now, and if we rerun our code, if we refresh here, everything should still work. Yes, everything is still working as expected. Okay, great. So we can clean this up. Okay, the next step. We have this client now. We want to configure it. So in this case, we have this API part we don't really want in here, so we can remove it here. But we still need to configure it somewhere. So instead, let's make a service provider. Make provider, and we call this NewsServiceProvider. So this service provider in here, let's look at it.
somewhere. So instead, let's make a service provider. Make provider, and we call this NewsServiceProvider. So this service provider in here, let's look at it. We will register the service in this app, singleton, and we're going to register the NewsClient in here. class, function, let's say return new NewsClient. Everything now is still working the same way as we did before. However, we're now using the singleton pattern here. The reason for this is that it's important that we don't want to create a connection with the client every time. For example, if we need to do an extra call to do the authentication, you don't want to do it every time you need to use this NewsClient. You just want to do that once, and then from there on, keep working with that. So now that we have
want to do it every time you need to use this NewsClient. You just want to do that once, and then from there on, keep working with that. So now that we have our NewsClient, we can configure it here. Like we said before, we have this URL, right? We can just specify the base URL here, and this is the base URL that every consumer of this API or this NewsClient will use as the basis to do requests. So that's why we can remove it from here. We don't need it here anymore. Okay, next step. So this should actually work now, right? Because we already specified the base URL. So if we're now refreshing here, it should work as expected. So now it says it couldn't resolve top headlines. So that's... Okay, so the reason why this is not working anymore is because it's trying to call
expected. So now it says it couldn't resolve top headlines. So that's... Okay, so the reason why this is not working anymore is because it's trying to call this URL top headlines, but it doesn't know the front part anymore. And while we did configure it in here, we didn't tell Laravel to load the service provider. So instead, we need to go to our app.php config file and load it in here. So just copy this line and replace it with NewsServiceProvider. So if we don't refresh, everything should work as expected. See, we get all the results back again, and everything looks fine. Okay, so the next part is removing this API key from here. So for that, we need to go back to the documentation, and I think we have this authentication endpoint, yes? So they say there are three ways of doing
Testing and Mocking Client13:54
they have to cache, sometimes they don't, so now it's slow. Okay, this is working really great now, and this really makes it easy to reuse this on different parts. It's basically only this part, and you can even move this to a separated method and just pass in the URL, and you're done with that. So let's just call it Homework. So how do we write tests for this? The reason that we need the service provider is so we can replace the client with something else, so we can run it in our tests. So if I'm creating a test here, so let's create a test in the feature one, so new ServiceTest, and we're going to extend the TestCase class, testFetchingHeadlines. So we're going to test these things here, set up here as protected, yes. So we have one thing that we need to do here, because we're
class, test fetching headlines. So we're going to test these things here, set up here as protected, yes. So we have one thing that we need to do here, because we're calling the service in this case, we need to create the service. So in this case newsService is app\News. So because this is the class we're working with, and it's the only class we're working with, so it's fine in here to do it like this. So let's make this private, and this is a newsService as well. So now we all understand each other. So whenever I call newService, and I call headlines, I expect to get a collection back with 20 results, right? Because that's what we're already getting here, we always get 20 results back, so I expect it here as well. So this assertCount, 20 results. So if I would run this test, everything should
already getting here, we always get 20 results back, so I expect it here as well. So this assertCount, 20 results. So if I would run this test, everything should pass in here, right? So let's just run it, and it passes. If I run it again, so now we can see why we want to mock this API out, because we're talking to the API and it takes more than five seconds to run. If I run it again, now it's fast, 500 milliseconds, but 500 milliseconds is still very slow for a test, right? Because it's half a second. If you have like 100 tests, you need to wait a very long time. So we want to speed this thing up. So we can do one thing here, and that is mock out the client that we're using. So we're gonna mock out this NewsClient. So let's create a method that we can call to swap out the method, so swap
that is mock out the client that we're using. So we're gonna mock out this news client. So let's create a method that we can call to swap out the method, so swapNewsClient. And we have to call it in here, this swapNewsClient. To mock something, we need a mockHandler. So that's how Guzzle works. So we can create a mockHandler here, new mockHandler. And to the mockHandler, we can add responses, so Laravel, or in this case, the application knows what we need to do. So then we need to create our client, which is a news client in this case. And we need to be specific here about one thing, which is the handler. So we're gonna override the handler. As you can see, we don't specify any base URLs or headers or whatever, we just have to specify the handler here. So we can
gonna override the handler. As you can see, we don't specify any base URLs or headers or whatever, we just have to specify the handler here. So we can create a handler stack here, create, and we just pass in a mock handler. And then we have a client now with our mock handler, so it's already mocked. And we just have to tell the API, or tell Laravel now, that we need to swap this. So we can say instance, and then the instance that we registered in the container is this. And we just call it client. So this is the reason why I'm using the NewsClient here as a class. I could just use Guzzle, but that means every Guzzle client will be replaced. And now we have a specific Guzzle client that will be replaced with our own configuration. And
means every Guzzle client will be replaced. And now we have a specific Guzzle client that will be replaced with our own configuration. And because we don't use a normal string, we just use the new client in here. We can also be really specific about what we're replacing and how this looks like. You could use a string instead, but I still find this more readable. You can just click through to the class. It gives you so much benefits. And finally, we need to return the mockHandler. So return the mockHandler. So the reason we need this mockHandler is because we need to tell the mockHandler what kind of responses we're going to return here. So in this case, mockHandler, we add the field here, which is private as well. So in this case, we swap out the client. We register a new
we're going to return here. So in this case, mockHandler, we add the field here, which is private as well. So in this case, we swap out the client. We register a new service. So if we come in here, this newsClient, this will now be the swapped client. So we already swapped it before here. So this new service now contains the swapped client. So if we then run the test, you can see that it will still run a request using that newsClient. And then the test will fail. So let's look at what the test will do if we run it. So now it says the mockQueue is empty. So it expected some kind of API call, or at least an HTTP call, and it didn't do any HTTP call at all. Because we didn't tell it to do any HTTP call. So in this case, we can say mockHandler, append. So we can tell the mockHandler to
didn't do any HTTP call at all. Because we didn't tell it to do any HTTP call. So in this case, we can say mockHandler, append. So we can tell the mockHandler to append a certain response. And from there, do something with that. So we can say a new Response, and we need to be specific here in CastleResponse. Now we can tell it the status, so 200. There's no headers we need to specify. And we need to specify some body. And this always needs to be JSON, so we can json_encode it. And we give it an empty array in this case. So if I run it again, you can see that the test will fail, because it can't find the articles index anymore. And that's correct, because in this case, we are specific here saying we get the body back, and then we say give me articles of this body. But there's no body in here.
same. So if we now rerun the test, so you can see we append this response. We can use an array here, so that's really nice. And then we just call the endpoint, and then this response will be used in the client. Run it again now, it will fail because now it says it doesn't match 20, because it will only return one. So instead we just move this to one, run it again, and then we get green. As you can see, this time is going massively faster than we did before. Now 100 milliseconds instead of 500 or 5 seconds. What else can we do? A service class or multiple service classes that use the same client, right? And you don't want to add this everywhere, this method, you don't want to copy it everywhere. So I normally move this to a trait. So let's just copy this over. I used to use this pattern where
everywhere, this method, you don't want to copy it everywhere. So I normally move this to a trait. So let's just copy this over. I used to use this pattern where we, I say use worksWithNewsClient, because this makes it really readable. So in our test we can make a new directory called support, or let's just call it support. A new class called WorksWithNewsClient, and this is actually a trait. And in here we have this method that actually mocks the, or in this case, swaps out the client for us. You can say here worksWithImport, that's it. So this method still is the same, right? We just call this method now, and it mocks out our handler, our newsClient, and returns the mock handler for us. So we get this mock handler still, then we can work from here. And we can even move this response
our handler, our news client, and returns the mock handler for us. So we get this mock handler still, then we can work from here. And we can even move this response maybe to the trait as well. So in this case we're mocking one single response, so we can probably say public function mockSingleArticleResponse. And this one returns a Response object, right? And we can't use this here, we have to return it here, just return it here. Let's put return in front of it. And then instead here we can say $this->mockSingleArticleResponse(). So now our tests are a lot more readable. We only have now three lines that actually help us to test this out. So if you run it again, it should actually work correctly. Yes, it does. So we can add some other assertions in here as well. We can say assertInstanceOf(Carbon,
you run it again, it should actually work correctly. Yes, it does. So we can add some other assertions in here as well. We can say assertInstanceOf Carbon, because we have this value called published, which should be a Carbon object, right? And then we have, for example, we return here the name of the author is Megan Cannon. I can just set it here, the author. If we then run it again, and it fails because, ah sorry, assertEquals. This should be equals, yeah. Run it again. Okay, now it still succeeds. So we already tested a lot of the responses now and then we get results back. So that's really good. So there's one more thing I want to do, which is cleaning up this ServiceProvider stuff. We have this base URL. We can put this here as well. However, you don't want to hard code this.
want to do, which is cleaning up this service provider stuff. We have this base URL. We can put this here as well. However, you don't want to hard code this because this might get replaced sometimes and this authorization token might get replaced and you don't really want this in your version control as well. So let's create a config file called news.php. And in here we have a PHP file which returns, let's call this the base URI. Let's call it access API token. I think that's better, API token. And in here we want to use the env helper and use API token. Default it's empty, which is good. And then for the base URL we can just probably put in the env here as well. NEWS_API_BASE_URI. And we can provide the default in here because this is not really sensitive data, but it's
just probably put in the .env here as well. News API base URI. And we can provide the default in here because this is not really sensitive data, but it's still good to have this here. I just like my enters here. And then we can replace this with something else. So instead we can say config('news.base_uri'). I can just say config('base_uri'). And this one is done, API token. Yeah, that's it. Okay, so now it fails because we didn't specify the key now in the .env file. So we have to go to the .env file on the bottom, say NEWS_API_TOKEN is this one. We don't need the quotes here. We then refresh. Everything should still work as expected. So I hope this helps you get an idea of how you can create an easy service class that works with an API and then write tests for it as well.
