Thursday, September 14, 2017

Ode to Sendgrid

I recently have been playing around with Sendgrid and it is pretty amazing. I have had some experience with sending email programmatically with AWS' SES product and it's nice, especially with its SNS integration, but Sendgrid's killer feature is its speed. I had about 6,000 emails to send, each one personalized with an individual URL and the person's first name. With AWS and a multi-threaded PHP program, I can send about 15 emails per second, which would have taken almost 7 minutes to process. With Sendgrid, I create a template in their system with template tags and then I create a massive data structure with up to 1,000 entries and then post it to the Sendgrid API. Rinse/repeat. I was able to send my 6,000 emails in 6 seconds! Sendgrid took care of the personalization on their end and I was seeing emails being delivered very soon after. Wow. Here's my script code:

I've only scratched the surface - there's lots more Sendgrid offers. One of the nice things is that you can get an activity feed and some nice stats after you send an email to see how people respond:

Activity Log, showing opens, clicks, delivered, etc.

Stat report, showing email performance.

If you're a Google Cloud customer, they offer a free trial with up to 12,000 transactional emails per month. Definitely worth a checking out!

Monday, June 5, 2017

Global Climate

Climate change has been an ongoing issue for about 20 years (maybe more). I was influenced early on by Michael Crichton's book, State of Fear, which posits that there's no real scientific evidence of global warming. Plus, it was just another thing for humanity to focus on, that we need something to stay fearful of. It's an interesting theory, especially if you consider that the current growing fear of terrorism has probably reduced the fear of global warming, mitigating any resistance to progress. A few years ago, Casey shared this punnet-like square video of all the various perspectives of are we going to do anything about it and if global warming is caused by humans or not. So regardless of what you believe and if anything can be done, the risk of doing nothing seems a lot worse than the risk of doing something and it ending up not being a thing.

So with that out of the way, the current news of Trump and the Paris Climate Agreement has me more aware of local efforts. If the US government isn't going to lead the way, then we need to do what we can on a local level. As a North Carolina citizen, we have an opportunity to keep our state moving forward towards more renewable energy sources. There's a bill that was just filed with the NC House that rolls back some of the goals (the minimum stays at 6% vs. growing to 12.5% by 2021). I'm not a lawyer, but it also looks like nuclear energy will be considered renewable, which probably means that Duke Energy could just buy energy from its own nuclear plants vs. other renewable energy sources.

So, if you're a NC resident and you want to do something, you can find your representative and express your opinion. You can also look at what your town is up to.

Thursday, January 12, 2017

Drupal 8 project killed

I realize I haven't updated my blog in awhile, because I've been in the thick of developing and rolling out our new site (check it out!). This was taking our existing Drupal 7 site for FSR Magazine and migrating it to this new site, which is responsive and better catered to deal with all food service news. Unfortunately, we ended up killing the Drupal 8 upgrade because it was just too complex, given our custom module codebase. D8 has a mindset change, where everything is an Entity and it takes a lot of OOP and YAML files to get things done.

I figure it would cost us hundreds of hours to upgrade to D8 and it was tempting to upgrade to D8 for the configuration management, Big Pipe, and better caching features. But that's time and money we didn't want to spend on the upgrade.

I've talked with some folks at my local Drupal User Group (shoutout TriDUG!) and several others are in the same boat, where existing sites aren't upgrading, but rather, they're doing new sites in D8.

At some point, we'll need to do something, but we're able to do what we need with D7. If pressed, it may be easier to port to Backdrop vs. upgrade to D8.

Monday, June 20, 2016

Entities (Porting Drupal 7 modules to Drupal 8)


Wow. Entities. What a mess. I really struggled with this. There's an example of a database-based module not using them and one that does use them. I think it boiled down to me that what I was representing were reports, which seemed very similar to nodes (in hindsight, perhaps this should have been a content type all along), so I should use entities.


As I look through examples, trying to figure out how to get my menu routing to work with my database entries, it appears that I need to set them up as entities. I say that, looking at the xmlsitemap and dbtng_example and content_entity_example from the examples module. So, let me back up and learn about D8 Entities. It looks like entities were introduced in Drupal 7 and there is a guide for that, but it's based on D7, so the concepts stuff is ok, but the implementation guidance is outdated. There's a link at the top to take you to a D8-specific guide. The guide says it's just a holder, but I'm going to give it a go. If nothing else, perhaps this post will help others.

First, entity types. Either you're setting up a configuration entity or a content entity. I think content entity fits for me. Next, looking at requirements section of the content type doc, I need to setup src/Entity/GatedContentEntity.php file with docblock stuff that will give the loader the information it needs. Hey, checkout this helpful hint about double quotes in the annotations doc (post #1 reference)! That annotations doc doesn't provide what all you need, but I'll take those 3 lines to start off with. I think this doc may list all of the properties you can use with ContentEntityType, but not sure which ones I really need and how to use them. This is when the examples/content_entity_example/src/Entity/Contact.php file helped. I first just used the core form & access handlers for now.

Random Aside: Sublime Text 3

I use Sublime Text 3 as my IDE and there is a PHPDoc plugin that makes life editing these annotations easier. Package Manager makes it easy to install plugins: just follow the installation instructions (and restart ST), then follow the usage instructions to pull up the Install Package option and then type phpdoc and select it to install. Then restart ST again and you'll be much happier. I think the same applies for Sublime Text 2, too. This Drupal doc has some other helpful Sublime Text tips.

Back to it...

So I found this great doc on how to create your own content entity. And whoa ... this comment pointed me to the Drupal Console project, which can jumpstart this a LOT. So I tucked my handcrafted code away and ran this command:
drupal --target="fnmd8.local" gect --module="gated_content" --entity-class="GatedContent" --entity-name="gated_content" --label="Gated Content"
The end result is about 10 files with lots of boilerplate fields that don't match up with what I'm using. Maybe I'm going about this all wrong. Maybe I don't need Entities? Time to take a break...

After the weekend

So I went back and added the preface above and I feel I need to stick the course with Entities. Perhaps this post will be totally rewritten after I totally understand them and and write a better guide that flows better. I need to just release this post because I'm about to flip back to some D7 work and will be taking another break. I'll pick this post thread back up again shortly.

Thursday, June 16, 2016

Porting custom module from Drupal 7 to Drupal 8 (databases, block view, URLs, and some menu work)

This is the next steps in getting my module working in Drupal 8. In the previous post, I got the block setup working, but it's just a generic "Hello World!" output. The block gets the most recent entries from a database table, so my next step is getting the database setup.


This doesn't seem to have changed from Drupal 7 to 8. You basically copy over the .install file. The hook_schema() takes care of it. BTW, one thing that is different is if you need to uninstall/reinstall the modules, you just need to drush pm-uninstall MODULE vs. drush dis MODULE ; drush pm-uninstall MODULE.

Once I had the database tables in place, I dumped my database tables from D7 and imported them into D8. NOTE: I probably need to either codify this (perhaps there's a way to hook into the migration system?) or keep notes about what tables to dump/import once everything's in place.

Now I needed to update my block to query the most recent entries in the table. db_query() is still supported in D8, however it looks like it's going away for D9, so if I wanted to future-proof my module even more, I could convert it to the new injection technique. So I created a storage class in my src folder. I downloaded the examples module and looked at the dbtng_example submodule and its DbtngExampleStorage class. I also found this answer on StackOverflow and the example posted in the select doc helpful. I ended up with a hybrid approach:

So a few things there. First, my post_date field was a legacy datetime field, so I had to convert the current time from a timestamp to the date format that mysql expected. format_date() is deprecated, so they recommend using Drupal::service('date.formatter'). Also, I make my __construct() method a little dynamic so that I don't have to pass in a connection, but it can support it if it is passed in.

Then my block build() method uses my storage class and calls the get_reports() method, passing in the block's configuration array. First, I add use Drupal\gated_content\GCStorage; to the top of my block plugin. Then my build() looks like this:
  public function build() {
    $storage = new GCStorage;
    $reports = $storage->get_reports($this->configuration);
    $build = array();
    kint($reports, $build);
    return $build;

Block View

So now that my block has the right set of reports to show, I need to show them. This takes me to the whole theming and templates stuff. The D7 version called theme() directly and there was a hook_theme and I had a template. The end result was a simple
and foreach report, a
and l(); call to link to the report. Kind of reminds me of item_list a bit, so I think I'll start there.

Ok, it looks like item_list looks about the same. My render array just needs to '#theme' => 'item_list'. But l() is gone. This comment pointed me in the right direction. So now it looks like this:
    $items = array();
    foreach ($reports as $report) {
      $url = Url::fromRoute('');
      $items[] = \Drupal::l(t($report['title']), $url);
    $build = array(
      'report_list' => array(
        '#theme' => 'item_list',
        '#items' => $items,
        '#attributes' => array(
          'class' => 'report-block',

Of course, these all link to just the homepage, which isn't very helpful.


So now I'm looking down another rabbit hole. The URL to my report is reports/[ID#]. But I use this module on separate domains, so the reports part is configurable. So I need to get that configuration setting to get the first bit of the URL. Actually, I think I can skip that since we'll be doing something different in our migration plans, so I can skip that part of the module. [postnote: if I do end up needing this, it looks like this shows how]

Also, I had this module all setup with pathauto to get a nice SEO-friendly public URL. Ok, so I need to setup the menu routing so the user can get to /gated_content and /gated_content/[ID#] and /gated_content/[ID#]/download. And I need to setup pathauto again.

First, all of those URL's were setup in D7 with type of MENU_CALLBACK, so according to this doc, I need to set them up in a .routing.yml file. This wasn't something DMU setup for me, so I did this from scratch. This was a great doc that showed some examples going from D7 to D8.

My first question is that I was familiar with named placeholders and how the name would be used to call a NAME_load() function to put together the argument that gets passed into the callback. But these examples are using book, which uses node and entity, so I need something that will use my own data type.

Stuff for the next post!

Wednesday, June 15, 2016

Porting Drupal Blocks from Drupal 7 to Drupal 8

We are looking into porting our site to Drupal 8 and we have over 30 custom modules, so it's a pretty big undertaking. I thought I'd blog about the adventures in the hopes of helping others tackle porting to D8 as well as notes for myself.

Back in April, I took one of our sites and used Drupal Upgrade module to get it moved into D8, following these instructions. I really haven't touched it since then, so I can't really remember how that went. So I just blew the dust off and upgraded from 8.0 to 8.1 in the process. One of the first things I noticed is that comments were turned off, so I had to navigate into the various content types and edit them and edit the comments field to change it from Closed to Open.

Then I was looking through our custom modules and trying to figure out where to start first. Some of our stuff builds on top of each other, so I need to look at the hierarchy to determine what is something that isn't dependent on other things.

One of my first steps was to create a custom modules folder and copy my D7 custom module into that. I then copied the folder as a backup. Then I used the Drupal Module Upgrader (DMU) to generate an upgrade info report as well as attempt the upgrade process. Then I renamed that folder to append -auto and then I created a new folder where I will start work in earnest.
cd /path/to/drupal8 ;
mkdir -p modules/custom/gated_content ;
cp -r /path/to/drupal7/sites/all/modules/custom/gated_content modules/custom/gated_content ;
cp -r /path/to/drupal7/sites/all/modules/custom/gated_content modules/custom/gated_content-orig ;
drush dmu-analyze gated_content --path=modules/custom/gated_content ;
drush dmu-upgrade gated_content --path=modules/custom/gated_content ;
mv modules/custom/gated_content modules/custom/gated_content-auto ;
mkdir modules/custom/gated_content ;
So my first custom module handles what we call "gated content," which is a collection of PDF files that our website users can request to download, which takes them to a request form and after they fill it out, it emails them a link to the PDF.

The module also has a block that shows the most recent gated content entries, which we include in our sidebar.

So I just wanted to start with the block and the block's settings.

The Block

One of the reasons I wanted to start from scratch is that this module is pretty big with lots of code that needs addressing to work in D8 and I'd like to incrementally get stuff working without having all the old stuff in there. So I copied the module's .info.yml and .permissions.yml files into my blank folder to start. No .module file. At least for now.

Blocks have a different way of doing things. You create a plugin. You don't need to point to your plugin through some .yml file. You create a src/Plugin/Block folder in your module and then create a php file for your plugin class. The filename needs to match PSR-4 specifications, which is the magic that autoloads all of this. So I called mine GCListBlock.php. One of my first lessons is that my namespace has to match the module name. My module name is gated_content and I initially used gatedcontent, which resulted in the block showing up in the list, but once you tried to place it in a region, it would result in an error.

Another odd thing is that the phpdoc is very specific with double quotes vs. single quotes. Use double quotes.

Using single quotes resulted in this error:
Doctrine\Common\Annotations\AnnotationException: [Syntax Error] Expected PlainValue, got ''' at position 18 in class Drupal\gated_content\Plugin\Block\GCListBlock. in Doctrine\Common\Annotations\AnnotationException::syntaxError() (line 42 of /Users/jason/Sites/devdesktop/fnmd8-dev/docroot/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php).
Another hiccup I ran across is I had t() calls through my block code that I had to change to $this->t().

I think there was some sort of 8.1 update, which wasn't reflected in the Blocks doc I was pointed to from the DMU report (I added a comment), that says $form_state is now a FormStateInterface. This also means you need to update accessing the form_state values to use $form_state->getValue('FORMFIELDNAME').

Finally, blocks don't just show up in the list of blocks on the blocks page like you might expect from Drupal 6/7 days. You have to click the Place Block button to pull up a list of available blocks and you should see it there. This is pretty neat because you can add a block more than once to a region or across multiple regions. I'm sure there's some way to differentiate the build based on its own order and region, but I don't have to worry about that for now.

The Settings

I initially thought I needed to create a settings yml file, but actually, since the settings aren't used outside of the block code, they can be all specified and used in the block's class. You can provide the defaultConfiguration() method to specify the default values (return an array of keys to values).

Bonus: Devel Module

One of the hiccups I faced was how to get the devel and dpm() calls working like I was used to in D7. One of the cool things I undercovered while getting devel setup is the webprofiler submodule, which adds a nifty toolbar at the bottom of the page with lots of development information and options.

I couldn't get dpm() (or dsm()) calls to show anything, so I stumbled across the kint submodule, which looks even better. You basically enable it and then use kint() vs. dpm(). One bonus is that kint() allows for multiple variables in the same call. It outputted the variable in a narrow region, but there's right arrow you can click on to open it in a new tab. And there's a useful stack trace beneath the output. One other thing I read is that dpm() doesn't have access to protected data, but kint() does.

Also, if you're not seeing output from either dpm() or kint() calls, try doing a cache-rebuild (drush cr). If that fixes it, you may want to setup local development configuration overrides so you don't have to rebuild manually every time.

Tuesday, October 13, 2015

Apex Candidate Forum for Nov. 2015 Elections

Totally different topic than technology, but this is another topic I care about - my government. I'm raising two young daughters and am interested in providing them with an environment where they can be safe and thrive. Our town has gotten a little bit of attention; it has definitely grown since we moved here in 2002. Tonight was a candidate forum for the local elections, which I assert means more in my day-to-day life than any other little election going on. With the growth going on here, there's definitely an increased interest in the races. When I originally registered for the event, it was going to be held at the town hall, but they soon changed it to a new high school auditorium when it was apparent there was going to be a large crowd. I would estimate there were about 300 people in attendance.

There's a mayor race with 2 candidates and a town council race with 2 spots and 5 candidates.

Town Council Forum

The town council was first up. There were five candidates and they were given 90 seconds to make opening remarks and then the moderator (WRAL's Ken Smith!) would ask five questions (pre-selected, no audience interaction), giving each candidate 90 seconds to answer the question. After the five questions, each candidate would be given 90 seconds for closing remarks. Candidates were not able to challenge or address each other. This was a forum just to give the voting public an insight into their approaches.

The candidates talked about smart growth, using our #1 ranking and attraction to be more selective with the homes & businesses that want to come here. They saw a need for better roads and more water/sewer. There's an upcoming bond on the ballot that most (all?) candidates supported. It also seems like the Land Use plan needs to be updated.

Here are my notes from the question session:

  1. What steps would you take to manage the influx of new residents?
    • Helton: Invite more businesses. Don't say no to all opportunities.
    • Jensen: Manage subdivisions with infrastructure. Incentivize businesses. Parks (called out Pleasant Park w/ 93 acres)
    • Lassiter: Sustainable growth. Use land use plan. Strict standards/high fees for builders.
    • Moyer: Set standards. Don't have to say Yes to all. Bring business to 540/64. Non-residential growth.
    • Xavier: Managed growth. Keep eye on density.
  2. What are your top two infrastructure challenges and how would you address them?
    • Jensen: Roads. Called out Kelly Rd/Olive Chapel Rd as a problem. $50M bond coming up to provide bridge over Salem St. for Peakway. Sewer/water.
    • Lassiter: Roads. Approve transportation bond. Says it won't raise taxes. He sponsored the bill of getting out of the power business and says it saves the consumers money.
    • Moyer: Traffic. Bond to help Peakway. Partner more with the DOT.
    • Xavier: Roads. Annexation needs to include sewer/water. Avoid being in the business of provider power and implement it quicker.
    • Helton: Bond. Sewer/Water is lifeblood. Looking at history, the $2M bond for sewer/water helped support the whole Beaver Creek Common shopping area.
  3. Would you support bond for Pleasant Park, even if it increases taxes?
    • Lassiter: Would need concrete plan.
    • Moyer: Agree. Would like to see plan tap into private funding. Let the public decide on the bond.
    • Xavier: Fiscal conservative. No Bond. Wants senior center before a park.
    • Helton: Needs to see a plan.
    • Jensen: Need a plan. Voters will decide.
  4. What specific role should the council play when recruiting business?
    • Moyer: Land Use Plan. Incubator.
    • Xavier: Work proactively. In unison w/ builders and residents. Facilitate/manage process.
    • Helton: Untie the hands of the economic development department. Allow them to offer incentives, such as free water/sewer for a year.
    • Jensen: Revise land use plan. Add more business to the plan. Preplan infrastructure (water/sewer), so they can have it ready when a business shows up and ready to move in.
    • Lassiter: He seemed more resistant, thinking we're fine w/ the residential percentages.
  5. Would you support a grant to match new investments for existing, expanding businesses?
    • Xavier: Works elsewhere, even w/ Dell. Yes
    • Helton: Yes. It's the right thing.
    • Jensen: 80% of our growth comes from existing businesses, so yes, we need to support that.
    • Lassiter: Skeptical, but if it's proven and ironclad, he would support it. Also if it didn't mean raising taxes. We already have a pretty good incentive with our workforce, open spaces, etc.
    • Moyer: Invest in infrastructure.
In closing, Moyer seemed to get a dig into Lassiter by saying he's not using the council race as a stepping stone (Lassiter is rumored to run for higher office next). Lassiter was thankful to everyone and wanted to remind folks he's a fiscal conservative. Jensen stressed that we need more employers and a senior citizen center. Helton wrapped it up by saying we need to take some risks and that his mother-in-law is a great chef, so he's not going anywhere.

Mayor Forum

There are two candidates. Same format as the town council, but with six questions. Even with an additional question, they were done in less than half the time it took to field five questions to five candidates.

Olive opened that he's been here for 45 years and back then, the population was 1500. He's seen the downtown whither and then revitalize. Wilkie moved here in 1990 and wants to maintain our strong sense of community.
  1. How will you support the council when there's conflict?
    • Olive: Unify council. Stop bickering and personal attacks. Work to find common ground. Meet with each member and find out what are their top five things they want to accomplish.
    • Wilkie: The mayor is the CEO of the town. Represents the residents. Bring consensus.
  2. What do you want to accomplish?
    • Wilkie: Economic development. Help small businesses grow.
    • Olive: Hold public meetings to gather feedback. Election districts to make sure all areas of Apex are equally represented. Unique shops in downtown. No chains.
  3. How would you represent Apex in state or national venues?
    • Olive: Work with Capital Area Metro to make sure Apex traffic is represented in overall plan.
    • Wilkie: Work with DOT on roads.
  4. Businesses can't find space in Apex - how would you handle this?
    • Wilkie: We should target 30% commercial tax base. Let them know we'll step up to help with infrastructure. Business-friendly.
    • Olive: We're currently 82% residential. We need to review the plan, which should be derived from vision.
  5. Explain vision of economic development.
    • Olive: It's going to come because of the #1 ranking. Need to work with existing businesses.
    • Wilkie: Show businesses that we have the talent pool already, such as the academies in the high schools.
  6. Strategies for keeping up with being the best place to live?
    • Wilkie: Being selective about businesses/subdivisions. Temper growth and selecting quality businesses and houses.
    • Olive: Need to cast a vision for 20 years out. Insist on quality.
In closing, Wilkie said that Apex has a great staff and wants to support and use them. She's unaffiliated and so she doesn't have any party agenda. She wants to keep taxes low and businesses strong. Olive talked about breaking down his candidacy into three sections: Vision (history of working across party lines), Leadership (Cisco leadership, business school as an adult, and no party agenda, just an Apex agenda), and Experience (6 years on the planning board and 4 years on the council).

There's a local Apex Voter's Guide website that's pretty good, where you can get some more information. Don't forget to vote on November 3rd!