I've been wanting to finish this post since I tweeted about my 1-Year Anniversary at Metal Toad. It has been the best job ever. I've already talked about how they devote time to R&D and how they respect developers estimates and work with clients that understand and value quality and expert service. The best part is that things have even improved since then. We moved to a new space with its own bathrooms, kitchen, and a foosball table. They implemented profit-sharing and a remote work policy (aka work from home), which includes WFH Fridays.
I get to work with really smart people who are equally humble, producing high quality work that I can be proud of and am given time to learn new things and keep my skills sharp. As with any job, there are days that may be more stressful or frustrating, but those are few and made easier by working with people that listen and respect you. So, needless to say, I am extremely happy with my job.
JoJoNAloha the Web Developer
Web development tips, tricks, problems, bugs and more. Focused primarily in PHP, MySQL, HTML, CSS and Javascript.
Saturday, September 8, 2012
Monday, April 9, 2012
Drupal Entities are the New Content Type
This may be old news to some, but for many Drupal entities are still a very new concept. While I could spend all day talking about the entity system, I just wanted to highlight what it is and how to get started.
In a nutshell, the entity system allows you to access things like nodes and users from a common interface, as well as define your own types of entities. To define your own entities, you implement hook_entity_info() in your module.
Here is a quick example form a sandbox module I'm working on.
Implement a couple views hooks and your entity has all the potential as it would if you had created a new content type, without cluttering up the content pages. For further reading, see The what, where and when of Drupal entities or if you are interested feel free to browse my sandbox entityqueue module on github.
In a nutshell, the entity system allows you to access things like nodes and users from a common interface, as well as define your own types of entities. To define your own entities, you implement hook_entity_info() in your module.
Here is a quick example form a sandbox module I'm working on.
/**
* Implements hook_entity_info()
*/
function entityqueue_entity_info() {
$types['entityqueue'] = array(
'label' => t('Entity Queue'),
'base table' => 'entityqueue_queue',
'load hook' => 'entityqueue_load',
'uri callback' => 'entityqueue_uri',
'fieldable' => TRUE,
'entity keys' => array(
'id' => 'qid',
'label' => 'title',
),
);
$types['entityqueueitem'] = array(
'label' => t('Entity Queue Item'),
'base table' => 'entityqueue_entity',
'load hook' => 'entityqueueitem_load',
'uri callback' => 'entityqueueitem_uri',
'fieldable' => TRUE,
'entity keys' => array(
'id' => 'qeid',
)
);
return $types;
}
In my example, I'm not using bundles or revisions, both can be extremely useful depending on your need, but I am using the Field API, which makes it easy to allow site builders to add fields to my entities to extend their usage, just like you would use CCK in Drupal 6 to extend nodes (now the Field API in Drupal 7). From there, in your form builders, you use field_attach_form() to add these extra fields to your form, as seen at the end of node_form() and when you save your entity, you use field_attach_presave(), field_attach_insert() and field_attach_update() to handle insertion and updating values for those fields, and don't forget to use field_attach_delete() when handling deleting one of your entities./**
* Save a queue.
*/
function entityqueue_save(&$queue) {
$transaction = db_transaction();
try {
// Load stored entity, if any.
if (!empty($queue->qid)) {
$queue->original = entity_load_unchanged('entityqueue', $queue->qid);
}
field_attach_presave('entityqueue', $queue);
if (!isset($queue->is_new)) {
$queue->is_new = empty($queue->qid);
}
module_invoke_all('entity_presave', $queue, 'entityqueue');
// Save the queue.
if ($queue->is_new) {
drupal_write_record('entityqueue_queue', $queue);
$op = 'insert';
}
else {
// For existing queues, update the queue record which matches
// $queue->qid.
drupal_write_record('entityqueue_queue', $queue, 'qid');
$op = 'update';
}
// Save fields.
$function = "field_attach_$op";
$function('entityqueue', $queue);
module_invoke_all('entity_' . $op, $queue, 'entityqueue');
// Clear internal properties.
unset($queue->is_new);
unset($queue->original);
// Clear the static loading cache.
entity_get_controller('entityqueue')->resetCache(array($queue->qid));
// Ignore slave server temporarily to give time for the
// saved entity to be propagated to the slave.
db_ignore_slave();
}
catch (Exception $e) {
$transaction->rollback();
watchdog_exception('entityqueue', $e);
throw $e;
}
}
To load your entities, complete with any attached fields, you can use entity_load(), which you may write a couple wrapper functions to make it even simpler, like so./**
* Load an entityqueue object from the database.
*/
function entityqueue_load($qid, $reset = FALSE) {
$qids = (isset($qid) ? array($qid) : array());
$conditions = array();
$queue = entity_load('entityqueue', $qids, $conditions, $reset);
return $queue ? reset($queue) : FALSE;
}
/**
* Load multiple queues.
*/
function entityqueue_load_multiple($qids, $reset = FALSE) {
$conditions = array();
return entity_load('entityqueue', $qids, $conditions, $reset);
}
I like using node_load() and node_load_multiple() as examples as well, usually the best examples can be found right in core, just do a search for hook_entity_info() and look at the different callbacks and menu items callbacks.Implement a couple views hooks and your entity has all the potential as it would if you had created a new content type, without cluttering up the content pages. For further reading, see The what, where and when of Drupal entities or if you are interested feel free to browse my sandbox entityqueue module on github.
Sunday, March 11, 2012
PHP 5.4 Traits
With the recent release of PHP 5.4 there are several new features that will have PHP developers talking. The one that I immediately find most intriguing is Traits. Traits are a new feature in PHP's object-oriented toolbox that allow a form of multiple inheritance that was not possible before.
Since you cannot declare an instance of a Trait and unlike an Interface you can define base methods along with your abstract methods, it seems to me that Traits can be thought of as a special type of abstract class.
One of the more complex examples from PHP.net demonstrates how to resolve the issue of name-conflicts when inheriting from multiple Traits.
This is a contrived example, but consider the following code. In order to test this out for myself, I followed the instructions for installing PHP 5.4 on Ubuntu.
Since you cannot declare an instance of a Trait and unlike an Interface you can define base methods along with your abstract methods, it seems to me that Traits can be thought of as a special type of abstract class.
One of the more complex examples from PHP.net demonstrates how to resolve the issue of name-conflicts when inheriting from multiple Traits.
<?php
trait A {
public function smallTalk() {
echo 'a';
}
public function bigTalk() {
echo 'A';
}
}
trait B {
public function smallTalk() {
echo 'b';
}
public function bigTalk() {
echo 'B';
}
}
class Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
}
}
class Aliased_Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}
?>
Using the new "insteadof" (not to be confused with
"instanceof") you can specify which implementation of a method to use,
and with "as" you can essentially rename a method to use a new name in
your implementation. While this does solve the issue of name-conflicts, I think that we will start to see a whole new set of problems as packages start to implement traits in their design patterns.This is a contrived example, but consider the following code. In order to test this out for myself, I followed the instructions for installing PHP 5.4 on Ubuntu.
<?php
trait Revision {
public function saveData() {
$data = $this->getData();
// Store data, maybe add a revision timestamp and author
}
/**
* @return array
* An array of data to be stored.
*/
abstract public function getData();
}
trait Printer {
public function printData() {
$data = $this->getData();
// Format data and output
print_r($data); // Just for our example
}
/**
* @return array
* An array of data to be output.
*/
abstract public function getData();
}
class MyClass {
protected $data; // An array
use Revision, Printer {
Revision::getData insteadof Printer;
Printer::getData as getPrinterData;
}
public function __construct($data) {
$this->data = $data;
}
public function getData() {
return $this->data;
}
public function getPrinterData() {
// Perhaps we hide some data from being printed before returning it.
$data = $this->data;
unset($data['private']);
return $data;
}
}
$data = array(
'name' => 'Instance of MyClass',
'private' => 'Something that is stored somewhere but should not be output.',
);
$my_class = new MyClass($data);
$my_class->printData();
?>
What should happen when you call printData()? Does it call getData() or getPrinterData()? The answer is that it calls getData(), which is not our intended result as our "private" data is output as well.
Array
(
[name] => Instance of MyClass
[private] => Something that is stored somewhere but should not be output.
)
From what I can tell there isn't a way to call the renamed method as it was aliased from other methods in the Trait. While this isn't a completely realistic example, I think it demonstrates that just because you can easily alias conflicting method names with Traits, doesn't mean you should become lazy with your namespacing.
Wednesday, February 22, 2012
Unit Testing Using Generated Test Cases
This is a follow-up post to my super simple php unit test base class that I wrote in a bind for Facebook's HackerCup. In that post I provided a real test I created using my base class jphpunit which included a test using the problems sample input and output and another test that used a generator. Today I will share the generator I used to help me test my code against the full set of constraints of the problem. This or a similar pattern can be used in your tests, in fact I highly recommend PHPUnit.
I created an abstract class for my test case generator so that I can use the same test pattern for each of the Facebook problems.
Here is an example of the class in use.
Finally, here is how the generator can be used in a unit test. I'm using my jphpunit class, but it should work as-is in a PHPUnit test as well.
I created an abstract class for my test case generator so that I can use the same test pattern for each of the Facebook problems.
<?php
/**
* Generates input and output data for use in unit tests.
*/
abstract class fbbase_base_generator {
private $answer = '';
private $test_cases;
public function __construct($numTests = 20, $options = array()) {
// Generate test cases
for ($i = 1; $i <= $numTests; $i++) {
$this->test_cases[$i] = $this->_createTestCase($options);
}
date_default_timezone_set('America/Los_Angeles');
}
public function saveFile() {
$filename = 'data/generated_'. date('Y-m-d_H-i-s') .'.txt';
$input = count($this->test_cases) ."\n";
foreach ($this->test_cases as $i => $test_case) {
$input .= $test_case['input'] ."\n";
}
file_put_contents($filename, $input);
return $filename;
}
public function getCorrectAnswer() {
if (!empty($this->answer)) {
return $this->answer;
}
$answer = '';
foreach ($this->test_cases as $i => $test_case) {
$answer .= 'Case #'. $i .': '. $test_case['output'] ."\n";
}
$this->answer = trim($answer);
return $this->answer;
}
/**
* @param array Any options that you want your test
* certain conditions are passed directly to this
* function from the constructor.
* @return array An array of the test case, should
* have 'input' and 'output' key indexes. Input is
* the line that will go in the input file, output
* is the expected output after the "Case #{$i}:",
* do not include leading or trailing whitespace
* unless it is expected as part of the output, as
* they will be added for you.
*/
abstract protected function _createTestCase($options);
}
This base class allows me to define the _createTestCase() method with the details of the problem at hand without having to worry about re-writing the code that will remain the same no matter what the problem is.Here is an example of the class in use.
<?php
/**
* Generates input and output data for use in unit tests.
*/
require_once('fbbase_base_generator.php');
class alphabet_soup_generator extends fbbase_base_generator {
protected function _createTestCase($options) {
$alphabet = 'BDFGIJLMNOQRSTVWXYZ'; // A partial alphabet so we don't
// accidentally create more instances of HACKERCUP
$soup = array(); // An array of all letters in our soup
// Add letters for HACKERCUP
$hacker_cups = rand(0,30);
for ($i = 0; $i < $hacker_cups; $i++) {
$soup[] = 'H';
$soup[] = 'A';
$soup[] = 'C';
$soup[] = 'K';
$soup[] = 'E';
$soup[] = 'R';
$soup[] = 'C';
$soup[] = 'U';
$soup[] = 'P';
}
// Choose a partial set of our HACKERCUP letters to include
$set = rand(0,5);
$num_sets = rand(1,10);
for ($i = 0; $i < $num_sets; $i++) {
// Add letters from our partial alphabet
$soup = array_merge($soup, str_split($alphabet));
// Add our partial set of HACKERCUP letters
switch($set) {
case 0:
$soup[] = 'H';
$soup[] = 'A';
$soup[] = 'C';
$soup[] = 'K';
$soup[] = 'E';
$soup[] = 'R';
break;
case 1:
$soup[] = 'C';
$soup[] = 'U';
$soup[] = 'P';
break;
case 2:
$soup[] = 'H';
$soup[] = 'A';
$soup[] = 'C';
$soup[] = 'C';
$soup[] = 'U';
$soup[] = 'P';
break;
case 3:
$soup[] = 'K';
$soup[] = 'E';
$soup[] = 'R';
$soup[] = 'U';
$soup[] = 'P';
break;
case 4:
$soup[] = 'H';
$soup[] = 'C';
$soup[] = 'R';
$soup[] = 'C';
$soup[] = 'U';
break;
case 5:
$soup[] = 'H';
$soup[] = 'A';
$soup[] = 'C';
$soup[] = 'K';
$soup[] = 'R';
$soup[] = 'U';
$soup[] = 'P';
break;
default:
$soup[] = 'H';
$soup[] = 'A';
$soup[] = 'R';
$soup[] = 'K';
$soup[] = 'E';
$soup[] = 'P';
}
}
// Jumble our soup and then implode it to words
shuffle($soup);
$input = '';
foreach ($soup as $letter) {
$input .= $letter;
// Randomly add a space, approximately every 8 characters
if (rand(0, 8) === 0) {
$input .= ' ';
}
}
return array('input' => $input, 'output' => $hacker_cups);
}
}
In this case the problem was that you are given a list of phrases that are written in alphabet soup, and need to figure out how many times you can write HACKERCUP using only the letters from that phrase. Since it doesn't matter what the phrase is (or that is made up of real words), the test cases generated are kept simple. You also want to make sure you add enough complexity into the test cases to ensure your code meets some of the edge cases. For instance, I added extra characters from the word HACKERCUP, but was careful not to inadverently generate a test case that would have more instances then would be expected.Finally, here is how the generator can be used in a unit test. I'm using my jphpunit class, but it should work as-is in a PHPUnit test as well.
<?php
// Inside your unit test
public function testGenerated() {
$gen = new alphabet_soup_generator();
$fixture = $gen->getCorrectAnswer();
$input = $gen->saveFile();
$output = array();
exec('../alphabet_soup.php '. $input, $output);
$this->assertEquals($fixture, implode("\n", $output));
}
Saturday, February 18, 2012
A Super Simple PHP class for Unit testing
Since I was introduced to unit testing and test-driven development, I have become a big fan. Since most of my projects to date have been primarily PHP, I prefer PHPUnit as my unit testing framework.
To me the best part of unit testing is that good unit tests are quick, automated confirmation that everything is working as expected. Using test-driven development I can write a test to ensure that given a certain input my future code will return a given output. I often find writing my tests first also helps define what I want my application to do, it is an outline of what the code should do.
Then as I write my code, I can run the test often to ensure that I don't get false positives and it will also point out any syntax errors or typos in my code.
Recently I found myself without my trusty testing tool and without internet at the time to download it. I was about to enter Facebook's HackerCup and needed to be sure my answers were correct before submitting them. So I ended up writing a quick, very simple unit testing base class for my submissions. I call it jphpunit because it is my version of a php unit tester and I based much of how it is used and the output it generates on my experience with PHPUnit.
It only has one class, jphpUnit, and one external method, assertEquals($expected, $actual, $message = NULL).
Here is a quick sample of one of the unit tests I wrote using it for my HackerCup submissions. When working on the problems, I would copy and save the example input and output files to run a basic test on.
As you can see, I wrote a test that would generate other test cases based on the contraints of the problem to ensure my program met all contraints of the problem and not just the ones in the example. I will probably talk about this more in one of my next posts.
To me the best part of unit testing is that good unit tests are quick, automated confirmation that everything is working as expected. Using test-driven development I can write a test to ensure that given a certain input my future code will return a given output. I often find writing my tests first also helps define what I want my application to do, it is an outline of what the code should do.
Then as I write my code, I can run the test often to ensure that I don't get false positives and it will also point out any syntax errors or typos in my code.
Recently I found myself without my trusty testing tool and without internet at the time to download it. I was about to enter Facebook's HackerCup and needed to be sure my answers were correct before submitting them. So I ended up writing a quick, very simple unit testing base class for my submissions. I call it jphpunit because it is my version of a php unit tester and I based much of how it is used and the output it generates on my experience with PHPUnit.
It only has one class, jphpUnit, and one external method, assertEquals($expected, $actual, $message = NULL).
Here is a quick sample of one of the unit tests I wrote using it for my HackerCup submissions. When working on the problems, I would copy and save the example input and output files to run a basic test on.
<?php
/**
* alphabet_soup_test.php
*/
require_once('helpers/alphabet_soup_generator.php');
class alphabet_soup_test extends jphpunit {
public function testExample() {
$fixture = file_get_contents('data/example_output.txt');
$output = array();
exec('../alphabet_soup.php data/example_input.txt', $output);
$this->assertEquals($fixture, implode("\n", $output));
}
public function testGenerated() {
$gen = new alphabet_soup_generator();
$fixture = $gen->getCorrectAnswer();
$input = $gen->saveFile();
$output = array();
exec('../alphabet_soup.php '. $input, $output);
$this->assertEquals($fixture, implode("\n", $output));
}
}
As you can see, I wrote a test that would generate other test cases based on the contraints of the problem to ensure my program met all contraints of the problem and not just the ones in the example. I will probably talk about this more in one of my next posts.
Sunday, February 5, 2012
PHP Increment Operator Bug? ... Nope!
So this is one of those things that I ran into recently that I never knew, that I probably should have.
First the problem. I was working on a project and had a snippet of code similar to this:
I was expecting the output to be greater than 1, but for some reason it was always 1. To debug, I adjusted the code to see that my value should be being incremented.
Indeed it was, so why was $a_counter++ not working? Changing it to $a_counter += 1; worked fine.
At first I thought it was a PHP bug, and after doing a Google search, found a PHP Increment/Decrement bug report. Lo and behold, that is exactly what my problem was. My has_some_setting() function returns a BOOLEAN, not an INTEGER. And according to the PHP docs, this is not a bug.
So, the long and short of it is, if your increment/decrement operators aren't returning values you expect, make sure your variable is not a boolean. Also, it may be worth reviewing the Increment/Decrement operators on strings example.
First the problem. I was working on a project and had a snippet of code similar to this:
$a_counter = has_some_setting();
foreach ($arr as $key => $val) {
if ($val == $some_condition) {
$a_counter++;
}
}
echo $a_counter;
I was expecting the output to be greater than 1, but for some reason it was always 1. To debug, I adjusted the code to see that my value should be being incremented.
$a_counter = has_some_setting();
foreach ($arr as $key => $val) {
if ($val == $some_condition) {
echo $val . ' == ' . $some_condition . '
';
$a_counter++;
}
}
echo $a_counter;
Indeed it was, so why was $a_counter++ not working? Changing it to $a_counter += 1; worked fine.
At first I thought it was a PHP bug, and after doing a Google search, found a PHP Increment/Decrement bug report. Lo and behold, that is exactly what my problem was. My has_some_setting() function returns a BOOLEAN, not an INTEGER. And according to the PHP docs, this is not a bug.
Note: The increment/decrement operators do not affect boolean values. Decrementing NULL values has no effect too, but incrementing them results in 1.
So, the long and short of it is, if your increment/decrement operators aren't returning values you expect, make sure your variable is not a boolean. Also, it may be worth reviewing the Increment/Decrement operators on strings example.
Saturday, February 4, 2012
Nostalgia and Ramblings
It has been a while since my last blog post here. Re-reading my old posts makes me realize how little I knew then and how far I have come over the last few years. That is one of the things I enjoy most about being a web developer, it forces you to constantly learn and improve. I have always said that is one of my personal life-long goals; to never stop learning and improving.
I recently had a conversation with my mom about that too. She was saying that even as a little kid, I was always wanting to learn something new. So much so that she said they often couldn't keep up. As a father of two young girls, I'm amazed at how fast they learn and really how smart my girls are. Hearing some of the stories my mom was telling me about me at their age made me realize that this life-long goal of mine has really been ingrained in me from before I even realized it.
I find it very intriguing how smart children really are and how important enforcing the desire to learn is at such a young age. It seems like something we don't even realize most of the time. It also makes me think about conversations I've had with co-workers. There are so many times that we've worked with people that did not know something, and instead of trying to figure it out or learn, would ask somebody else. At my last job, (FYI, I've been working at Metal Toad Media since August), I was the person everybody asked. The thing that drove me nuts about it was that I often did not know the answer myself. I would use trial-and-error and/or Google and read to find an answer. That isn't to say people shouldn't ever ask, but it just seems that there are more and more people that don't have that same desire to learn. Even if they don't have that same passion, I can't seem to understand why they don't use the resources already available to them, and apparently I'm not the only one that feels that way. I'm not that mean-spirited to actually send somebody such a link, but I can see how some people might. I guess it just makes me see how important it is to instill and encourage my kids natural passion for learning and imagination.
Working at Metal Toad has been an eye-opener as well. My last job was so hectic and we were always under pressure to get things done "quick and dirty" and then "we can clean things up later". Later never came and while we were always told to do good work and only deliver something we could say we were proud of, it seemed to be an illusive goal. It is no wonder I grew tired of the way things were and grew to despise how I had gone from a company where I was able to learn and grow to a company that was behind the times with no plans to improve. Metal Toad has been such a contrast to that, it has literally been a culture-shock. Not only does Metal Toad encourage R&D time for each of its Developers, every week, but its pretty much a requirement. It allows Metal Toad to stay on the cutting-edge, and ensures that its Developers are the best in the business.
Along with R&D time, we are encouraged to blog regularly. In fact, at my first review, their only "complaint" was that I had only 1 blog post. The company holds blogging as a great way for developers to document what they have learned and over time it also helps establish the company and the individual as an expert in their field. Just like a doctor writes in a medical journal, a developer should blog.
This is something I have not been as good at, and in an effort to get better at it, I am going to start. At first, when reading my older posts, I was tempted to think that I would sound silly for "not knowing" and was discouraged. I'm realizing though, that it doesn't matter what I don't know now, it only matters that I will learn and get better. In fact, it will probably help me grow faster, and might, if I'm lucky, help somebody else.
So, expect more soon, both here and on my Metal Toad blog. This blog will probably be a little less formal (as you can probably see already), and will probably contain a few personal rants, definitely a few mistakes and a lot of learning.
I recently had a conversation with my mom about that too. She was saying that even as a little kid, I was always wanting to learn something new. So much so that she said they often couldn't keep up. As a father of two young girls, I'm amazed at how fast they learn and really how smart my girls are. Hearing some of the stories my mom was telling me about me at their age made me realize that this life-long goal of mine has really been ingrained in me from before I even realized it.
I find it very intriguing how smart children really are and how important enforcing the desire to learn is at such a young age. It seems like something we don't even realize most of the time. It also makes me think about conversations I've had with co-workers. There are so many times that we've worked with people that did not know something, and instead of trying to figure it out or learn, would ask somebody else. At my last job, (FYI, I've been working at Metal Toad Media since August), I was the person everybody asked. The thing that drove me nuts about it was that I often did not know the answer myself. I would use trial-and-error and/or Google and read to find an answer. That isn't to say people shouldn't ever ask, but it just seems that there are more and more people that don't have that same desire to learn. Even if they don't have that same passion, I can't seem to understand why they don't use the resources already available to them, and apparently I'm not the only one that feels that way. I'm not that mean-spirited to actually send somebody such a link, but I can see how some people might. I guess it just makes me see how important it is to instill and encourage my kids natural passion for learning and imagination.
Working at Metal Toad has been an eye-opener as well. My last job was so hectic and we were always under pressure to get things done "quick and dirty" and then "we can clean things up later". Later never came and while we were always told to do good work and only deliver something we could say we were proud of, it seemed to be an illusive goal. It is no wonder I grew tired of the way things were and grew to despise how I had gone from a company where I was able to learn and grow to a company that was behind the times with no plans to improve. Metal Toad has been such a contrast to that, it has literally been a culture-shock. Not only does Metal Toad encourage R&D time for each of its Developers, every week, but its pretty much a requirement. It allows Metal Toad to stay on the cutting-edge, and ensures that its Developers are the best in the business.
Along with R&D time, we are encouraged to blog regularly. In fact, at my first review, their only "complaint" was that I had only 1 blog post. The company holds blogging as a great way for developers to document what they have learned and over time it also helps establish the company and the individual as an expert in their field. Just like a doctor writes in a medical journal, a developer should blog.
This is something I have not been as good at, and in an effort to get better at it, I am going to start. At first, when reading my older posts, I was tempted to think that I would sound silly for "not knowing" and was discouraged. I'm realizing though, that it doesn't matter what I don't know now, it only matters that I will learn and get better. In fact, it will probably help me grow faster, and might, if I'm lucky, help somebody else.
So, expect more soon, both here and on my Metal Toad blog. This blog will probably be a little less formal (as you can probably see already), and will probably contain a few personal rants, definitely a few mistakes and a lot of learning.
Subscribe to:
Posts (Atom)