Well… 2021 is already feeling a little 2020ish to me, what about you? While I had high hopes of things being back to normal as far as travel, events, etc. It seems as though we still have a ways to go in that department.
TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP
Written by Amanda on January 11, 2021 •
6304 VIEWS
After the year we had… our new motto should be work smarter, not harder, in 2021? Am I right? Luckily, CakePHP community and core members are extremely helpful and constantly working to make baking easier. Here are some things that will assist you with your CakePHP projects….
Written by Amanda on December 21, 2020 •
6074 VIEWS
I never thought that I would be so excited to say… the year is coming to an end. What a year it has been. Let’s focus on some good things that happened for us this year.
Written by Yevgeny on December 08, 2020 •
7276 VIEWS
#[Attribute]
attribute.
In the following example we define an attribute class which allows us to specify what methods are allowed for controller actions.
Take note that in PHP 8 you can declare properties directly in class constructors, which is really handy for data classes.getAttributes()
methods to fetch a list of associated attributes.
The following example shows how we can add permissions checks for controller actions using attributes. In this example, the code is stored in beforeFilter of the controller class itself, but it could be an AppController or more generic solution.
Written by Yelitza on December 01, 2020 •
11701 VIEWS
CakePHP’s database Query Builder provides a simple to use fluent interface for creating and operating database queries. It can be used to accomplish most database operations in your application, and works on all supported database systems.
Query builders allow us to create database queries that work with any supported database, without having to worry about differences between SQL implementations in the different database systems.
The CakePHP query builder uses PDO parameter binding to protect your application against SQL injection attacks. There is no need to clean strings being passed as bindings.
For creating a Query object in CakePHP, the easiest way is using find() from a Table object. In CakePHP, queries are lazily evaluated, which means that they are not evaluated until any of the following actions happens: the query is iterated through a foreach, it’s called first(), all(), toList(), toArray().
You can check all the SQL queries that CakePHP is generating, you just need to enable the Database Logging. See here: https://book.cakephp.org/4/en/orm/database-basics.html#query-logging.
Let’s do a few samples using the Query Builder - this is the ER diagram of the database that we will be using for the queries. We have Orders, Products, Users and Items that will store the products sold in each order and the quantity sold.
Let’s create some queries using the Query Builder SQL Functions: https://book.cakephp.org/4/en/orm/query-builder.html#using-sql-functions. SQL Functions as part of the Query Builder are abstractions of some commonly used SQL functions, and they allow the ORM to choose the specific implementation your application needs based on the Database that is being used. For example, CONCAT is implemented in a different way in MySQL and Postgres, using concat() function will work if you use MySQL or Postgres
Imagine we want to build a report of the products sold, including the following:
$query = Table->find();
.
We want to get a report of the products sold and the current stock. Initially, we would need to build a subquery using ItemsTable where the information related to the products sold is present. Don’t forget to use identifier() when referencing any column. This will tell us the items sold per product. $itemsQuery = $this->Items->find() ->where(['Items.product_id' => $query->identifier('Products.id')]);
$query->select([
'display_name' => $query->func()->concat([
$query->identifier('Products.name'), ' - ', $query->identifier('Products.description')]),
]);
$query->select([
'quantity_sold' => $itemsQuery->select(['sum' => $itemsQuery->func()->sum($query->identifier('Items.quantity'))]),
]);
$query->select([
'income' => $itemsQuery->select(['sum' => $itemsQuery->func()->sum($query->newExpr('Items.quantity * Items.unit_price'))
])]);
$products = $query->cleanCopy()->select([
'id' => 'ProductDetails.id',
'price' => 'ProductDetails.price',
'quantity' => 'ProductDetails.quantity',
'display_name' => 'ProductDetails.displaye_name',
'quantity_sold' => 'ProductDetails.quantity_sold',
'income' => 'ProductDetails.income',
'stock' => $query->newExpr('ProductDetails.quantity - ProductDetails.quantity_sold'),
])->from([
'ProductDetails' => $query->cleanCopy()->select([
'id' => 'Products.id',
'price' => 'Products.price',
'quantity' => 'Products.quantity',
'display_name' => $query->func()->concat([$query->identifier('Products.name'), ' - ', $query->identifier('Products.description')]),
'quantity_sold' => $itemsQuery->select(['sum' => $itemsQuery->func()->sum($query->identifier('Items.quantity'))]),
'income' => $itemsQuery->cleanCopy()->select(['sum' => $itemsQuery->func()->sum($query->newExpr('Items.quantity * Items.unit_price'))]), ])
]);
SELECT ProductDetails.id AS id,
ProductDetails.price AS price,
ProductDetails.quantity AS quantity,
ProductDetails.display_name AS display_name,
ProductDetails.quantity_sold AS quantity_sold,
ProductDetails.income AS income,
(ProductDetails.quantity - ProductDetails.quantity_sold) AS stock
FROM
(SELECT Products.id AS id,
Products.price AS price,
Products.quantity AS quantity,
(CONCAT(Products.name, :param0, Products.description)) AS display_name,
(SELECT (SUM(Items.quantity)) AS SUM
FROM items Items
WHERE Items.product_id = (Products.id)) AS quantity_sold,
(SELECT (SUM(Items.quantity * Items.unit_price)) AS SUM
FROM items Items
WHERE Items.product_id = (Products.id)) AS income
FROM products Products) ProductDetails
Written by Ajibarra on November 24, 2020 •
8924 VIEWS
In just two days we will get a new PHP release, PHP 8. It’s been almost 5 years since PHP 7 was born and now we are about to enjoy the new major version, which will include some breaking changes and performance improvements. It comes with a lot of new features, including:
static
return type
mixed
type
DateTime
objects from interface
Written by Amanda on November 16, 2020 •
5726 VIEWS
Listen, although 2020 felt like it lasted 25 years, it’s still hard to believe that we are wrapping up this roller coaster of 12 months.
As companies prepare for 2021, crossing their fingers and wishing for a better Q1… it is important to start thinking about marketing strategies and plans for the next year. Without ideas and a solid goal for your company, it is very unlikely that things will change.
Reasons that making a marketing plan is important:
1. It organizes your goals and provides clear plans of actions to achieve them
2. It keeps everyone on track and on the same page
3. Promotes motivation and accountability
I know making a marketing plan can sound time consuming, and a little complex, but it doesn’t have to be! I am going to walk you through the steps of making clear cut goals and plans for 2021 (with some actual examples!).
Written by Rafael on November 09, 2020 •
8568 VIEWS
Traits are a mechanism for code reuse, the convention traits in CakePHP are suffixed with Trait so they can be discernible from classes or interfaces. Our book has a basic sample of Trait.
For me, writing code with Traits it's not a regular daily job, but last week I found myself writing a Trait for handling the Theme of an Application.
We need to define the theme on AppView and AppController, and we don't want to repeat ourself:
Beyond re-use code, we can use Traits to separate our code by responsibility.
We like this approach so much that our Users Plugin utilizes it.:
Basically, instead of creating a regular controller with many methods, we create Traits and group the actions by domain business.
Sometimes we need to override the functions, and we want add our custom logic after -before or replace the function on Traits:
If you wanna bake Traits more complexly I recommend you check out the PHP manual… there,you can see how to fix conflicts, change the visibility of methods and so much more.
That's all bakers! I hope you’ve found this information to be helpful!
Written by Rochamarcelo on November 02, 2020 •
7665 VIEWS
/users
and /users/[id]
to fetch data from database instead of in-memory data.
composer require cakephp/orm:~4.0
App.namespace
config.
First: add this line after the use
declaration. Don’t worry about this config now. We’re setting this to avoid error with PHP 7.4.
Configure::write('App.namespace', 'App');
Then add your database config key inside settings
array - in this case we’re using mysql database from a docker service:
'database' => [
'className' => \Cake\Database\Connection::class,
'driver' => \Cake\Database\Driver\Mysql::class,
'database' => 'my_db',
'username' => 'root',
'password' => 'secret',
'host' => 'mysql',
]
See https://github.com/CakeDC/slim-cakephp4-packages/blob/main/app/settings.php
$containerBuilder->addDefinitions
:
\Cake\ORM\Locator\TableLocator::class => function(ContainerInterface $c) {
$settings = $c->get('settings');
\Cake\Datasource\ConnectionManager::setConfig('default', $settings['database']);
return new \Cake\ORM\Locator\TableLocator();
}
See https://github.com/CakeDC/slim-cakephp4-packages/blob/main/app/dependencies.php
This service gets the settings we defined before,sets the configuration to CakePHP, and returns an instance of TableLocator.
We could use this service in any route action
with:
$locator = $this->get(\Cake\ORM\Locator\TableLocator::class);
$users = $locator->get('Users')->find()->all()->toArray();
But for this app it will make sense to use the TableLocator in a custom persistence class.
public function __construct(\Cake\ORM\Locator\TableLocator $tableLocator)
{
$this->tableLocator = $tableLocator;
}
Now we can use the tableLocator to fetch data:
$this->tableLocator->get('Users')->find()->all();
$this->tableLocator->get('Users')->get(10);
////
To make the app work correctly, we need to add the required method findAll and findUserOfId
See: https://github.com/CakeDC/slim-cakephp4-packages/blob/main/src/Infrastructure/Persistence/User/DatabaseUserRepository.php
Now that the repository persistence class was created, we need to connect
using dependency injection. Update the UserRepository::class
entry in repositories
with:
UserRepository::class => \DI\autowire(\App\Infrastructure\Persistence\User\DatabaseUserRepository::class),
https://github.com/CakeDC/slim-cakephp4-packages/blob/main/app/repositories.php
That’s it! Now we can access /users and see the users stored in our database.
I hope you’ve found this information to be helpful! Let us know!
For more information check out: https://github.com/cakephp/orm.
Written by Jorge on October 26, 2020 •
9049 VIEWS
One of the topics discussed in the community is the benefit of a unified, officially supported, CakePHP Queue plugin. Queues are used in most of the projects nowadays and are a central utility plugin. During the CakeFest 2020 event, there were also a couple direct references from speakers: (https://speakerdeck.com/josegonzalez/building-and-releasing-a-cakephp-plugin/?slide=15c) and this comment from Mark Story: https://cakesf.slack.com/archives/C172CS4TE/p1602257791377500. This motivated me to take a deeper look at the cakephp/queue plugin and write this blog post. Here at CakeDC we've been using queues for a looooong time. Initially in CakePHP 2, we've used plugins like CakeResque with redis or custom workers tied to Amazon SQS queues. Then in CakePHP 3 & 4 we've been using mostly https://github.com/josegonzalez/cakephp-queuesadilla with redis or mongodb backends. https://github.com/cakephp/queue First thing would be setting up the plugin in your project, we are going to use the example project we used in CakeFest 2020: https://github.com/cakephp/cakefest2020/#using-docker So after setting up the project and running it via docker compose, we can proceed to setup the plugin via composer. We will need to add it as a repository and set
"repositories": [
{
"type": "vcs",
"url": "https://github.com/cakephp/queue.git"
}
]
Then do
composer require cakephp/queue -W
And install some transport as stated in the official documentation https://book.cakephp.org/queue/1/en/index.html#installation
composer require enqueue/redis:^0.9
composer require predis/predis:^1
Ensure your redis server is up and running, you can check the commands sent to your local redis server using redis-cli monitor
Now we are ready to configure the queue, we'll create 1 default queue adding this to the config/app.php file
'Queue' => [
'default' => [
'url' => 'redis:',
],
],
Add a Job using `bin/cake bake job Example` and add some code to the execute method
public function execute(Message $message): string
{
$data = $message->getArgument('data');
// do some long operation with data
Debugger::log($data);
sleep(2);
return Processor::ACK;
}
I've added a command utility to enqueue a test message bin/cake bake command addJob
public function execute(Arguments $args, ConsoleIo $io)
{
$callable = [ExampleJob::class, 'execute'];
$arguments = ['id' => 1, 'data' => ['some' => 'data']];
QueueManager::push($callable, $arguments);
}
And finally we can start our worker using bin/cake worker
to pull jobs from Redis and process them using the ExampleJob::execute method
Here's all the example code created: https://github.com/cakephp/cakefest2020/tree/feature/cakephp-queue - for your reference.
Please note the plugin is still a work in progress and there is no stable release as of now.
It's looking great so far and we plan to include it in our next projects!
Written by Amanda on October 19, 2020 •
11392 VIEWS
Members of our team had the privilege of helping with CakeFest 2020 this year. One added virtual feature was the giveaways from CakePHP, these were done in the form of fastest-to-answer, trivia, or participation (random draw). One of the giveaway games was to share your favorite CakePHP plugin but like, how do we only pick one, right? Anyway… There was a lot of participation in this giveaway! A few people even named our CakeDC Users plugin as their favorite *cue blushing face*. But in all seriousness, I thought it would be a good idea to share with you some of the plugins that were named most useful/helpful by CakeFest attendees this year…. Like I mentioned, the CakeDC users Plugin: https://github.com/CakeDC/users Queue Plugin: https://github.com/dereuromark/cakephp-queue Bake: https://github.com/cakephp/bake DataTables: https://github.com/fheider/cakephp-datatables CakePHP-tools: https://github.com/dereuromark/cakephp-tools Authentication: https://github.com/cakephp/authentication CakePHP-image: https://github.com/josbeir/cakephp-image Fixturize: https://github.com/lorenzo/cakephp-fixturize CakePHP File-Storage: https://github.com/burzum/cakephp-file-storage Crud: https://github.com/FriendsOfCake/crud IDE Helper: https://github.com/dereuromark/cakephp-ide-helper Asset-Compress: https://github.com/markstory/asset_compress CakePHP Debug Kit: https://github.com/cakephp/debug_kit Plum-Search: https://github.com/skie/plum_search CakePHP API: https://github.com/cakedc/cakephp-api/ Bootstrap UI: https://github.com/friendsofcake/bootstrap-ui Trash: https://github.com/usemuffin/trash You can check out the full list of CakePHP Plugins at Plugins.CakePHP.org. Have you been utilizing these tools? If not, it may be a good idea to start… while each of these serve their own purpose, using all of them can make your baking process a lot easier. Perhaps your favorite didn’t make this list? Tell us about it… email HERE. Or, tweet us @CakeDC, @CakePHP. Thanks for baking!
Written by Amanda on October 12, 2020 •
6559 VIEWS
Taking a deep breath….. We have made it through another successful CakeFest event. We didn’t know exactly what to expect with a fully virtual event, as it opens the door for a list of things that can (and most likely will) go wrong. Would the speakers show up? Would the internet connections keep things from running smoothly? Would attendees enjoy the information? The answer to all of those questions is yes. The lineup this year was amazing, and we had speakers from 6 different countries! With the ever changing way of life, our team wanted to have a diverse group of speakers, with some talking about some pretty serious subjects - like Dr. Jennifer Akullian and the mental health issues faced in the tech world. Jen allowed for questions and how-to-handle scenarios, and worked through some tough issues with attendees. Workshops from Mark Story, Jorge Gonzalez, Mark Scherer and Jose Lorenzo provided incomparable information while building projects in real time. All of the talks provided useful information that we hope all attendees will take with them as they bake in the future. Now, does all of this bragging mean we didn’t have any issues? No. As I said in our opening, I think our group is pretty awesome and perfect, but the fact of the matter is, no one is. When bringing people together from all over the world, it is hard to test and check every connection multiple times. We had our fair share of internet lag, connection problems and audio issues. You know what we did? We worked together, fixed it, switched around the schedule and made it happen. Virtual CakeFest 2020 was a great success, and exceeded our expectations. We were able to gather bakers from all over the world, in real time, and host a live event! I couldn’t believe how interactive the audience was, and everyone is still baking together in our #cakefest chat channel. I hope everyone is as impressed with the turn out as our team is. I know what you’re thinking… when will the recordings be released. We are working on uploading, editing and releasing the videos ASAP. While we tried to find the best universal timezone, we understand that other obligations kept some from attending, and we want to share these videos with you quickly, so know that we are working on it. To our attendees: THANK YOU. Thank you for joining, supporting our framework and keeping CakePHP alive.
Written by Lubomir on September 28, 2020 •
7257 VIEWS
There are a lot of toys these days. Let me show you one very simple and powerful brick for your next revolutionary invention. Let's assume that you're either:
composer create-project \
--prefer-dist --no-interaction \
cakephp/app:~4.0 rest
cd $_
composer config \
minimum-stability dev
composer require \
cakedc/cakephp-api:~8.0
cp vendor/cakedc/cakephp-api/config/api_permissions.php.default \
./config/api_permissions.php
bin/cake plugin load CakeDC/Api
'database' => TMP . 'rest.sqlite',
'driver' => 'Cake\Database\Driver\Sqlite',
bin/cake bake migration create_books \
title:string:unique \
description:text \
price:integer:index \
available:boolean:index \
created \
modified
bin/cake migrations migrate
bin/cake server
Open http://localhost:8765/api/books in your browser to see the (empty) list of your books, then pick some RESTful Route and take a rest from the back end, even without backend coding, scratching who knows where, etc. You maybe have some SQLite databases laying around, and that would be also worth a try. I forgot to remind you to stop that 300s timer, sorry. You will get lost in time as well, once you'll realize what all is possible with the CakeDC/Api plugin using little to no configuration, and what dimensions are opening to you. If you combine its powerful multilayer services approach with solid implementation of your custom ORM classes embraced by CakePHP core and conventions, the results are amazing.
Oh, and a quick note for CakePHP 3.x developers - we like you, but please take your time to upgrade your toolset. Feel free to ask us for help, or use following warranty-limited commands:
composer create-project \
--prefer-dist --no-interaction \
cakephp/app:^3.9 rest
composer require \
cakedc/cakephp-api:~7.0
bin/cake plugin load -b -r CakeDC/Api
Written by Amanda on September 21, 2020 •
5807 VIEWS
In case we haven’t reminded you enough lately, CakeFest 2020 is less than a month away. While we have recently hosted a few virtual meetups, a fully virtual conference is uncharted territory for our team. I like to look on the bright side of things, so I will say how excited I am that bakers from all over the world will be able to join in and participate this year. Obviously, with international travel, this hasn’t always been the case for individuals. So my last CakeFest blog, I went into details of what to expect, and how the conference will (hopefully) run - smoothly. However, we’ve had a lot of emails and interest in hearing about what topics will be covered. Our lineup is excellent this year - so we wanted to share some things to look forward to:
Written by Rafael on September 15, 2020 •
14535 VIEWS
As you know, CakePHP announced the version 4.x last December.I recommend that you consider upgrading your applications to the next version, to keep up to date and get all the benefits. Now, let's see how to bake!
Written by Yevgeny on September 07, 2020 •
6857 VIEWS
SQL language for different databases has some differences, which could cause problems after migrations between these databases. Here, we collected some hints, problems could appear during migration from MySQL to PostgreSQL. We focus on the CakePHP model layer and query generation layer.
$query->where([‘Author.id = Article.author_id’])
which works fine till we dont need field alias quotes. In the case of migration to postgres, we might want to enable autoQuotes. In this case, we can use $query->newExpr()->equalFields(‘Author.id
, Article.author_id’)
.
ILIKE
.The case of old style conditions where method arrays straight forward, you’d just go with ILIKE
instead of LIKE
. But what if we want to use ILIKE
in builder methods... Here is that example:
return $query->where(
function (QueryExpression $exp) use ($field, $value): QueryExpression {
return $exp->add(new \Cake\Database\Expression\Comparison($field, $value, 'string', 'ILIKE'));
});
FunctionExpression
: $expr = (new FunctionExpression('CAST'))->setConjunction(' AS ')->add([$id, 'varchar' => 'literal']);
which generates expression like :id AS varchar
there :id is the placeholder for variable $id. This trick, used with literal, allows you to cast to any postgres type.
IS NULL
as string. So if array syntax is used, all CakePHP conventions must be followed. However, sometimes we should help the ORM and obviously wrap a field name with IdentifiedExpression
. Let's take a look back to the previous example, but now we want to type cast not value, but a field. The only solution to quote field name correctly is using this code:
$id = new IdentifierExpression($this->aliasField($field));
$expr = (new FunctionExpression('CAST'))->setConjunction(' AS ')->add([$id, 'varchar' => 'literal']);
“Events”.”time_to” - “Events”.”time_from”
...
With ListExpression, it can be done quite easy:
$diff = new ListExpression([new IdentifierExpression('Events.time_to'), '-', new IdentifierExpression('Events.time_from')]);
Hopefully these tricks will be as useful for your baking as they have been for mine!
Written by Yelitza on August 31, 2020 •
8400 VIEWS
The Cake Development Corporation team performs many code reviews. In fact, that is our starting point with every new client, as we offer free quick reviews. This is a good way to see where code stands, and how much work will need to be done to get it functioning properly.
One of the common errors we have found while doing Code Reviews of existing applications or just working with inherited code, it’s the way HasMany relations data is saved.
We have noticed that to save HasMany relations, some developers save the target relation, and then when the ID is retrieved after saving, they save one per one each item of the ‘many’ relation. There is no need to do this, as CakePHP can do all this in one single ‘save’! You won’t have any issue related to inconsistent data, because everything will be stored in one single transaction and your code will look much more clean.
Let’s see a quick and easy to follow example - We will have the following relations: ‘Users’ and one User could have many ‘Addresses’. We wish to save one user and this user will have two addresses.
First, you need to build the form in the proper way, the request data should follow the structure of your entities. The key in the form is the fieldName for the hasMany inputs. They must follow this format: {entityname}.{position}.{property}, for example: adddress.0.street_1
, adddress.0.street_2
, etc for the first item so store, for the second one: : adddress.1.street_1
, adddress.1.street_2
, and so on. More examples can be found here: https://book.cakephp.org/4/en/views/helpers/form.html#creating-inputs-for-associated-data.
<?= $this->Form->create($user) ?>
<fieldset>
<legend><?= __('Add User') ?></legend>
<?php
echo $this->Form->control('first_name');
echo $this->Form->control('last_name');
echo $this->Form->control('phone');
?>
<legend><?= __('Address 1') ?></legend>
<?php
echo $this->Form->control('addresses.0.street_1');
echo $this->Form->control('addresses.0.street_2');
echo $this->Form->control('addresses.0.zip');
echo $this->Form->control('addresses.0.city');
echo $this->Form->control('addresses.0.state');
?>
<legend><?= __('Address 2') ?></legend>
<?php
echo $this->Form->control('addresses.1.street_1');
echo $this->Form->control('addresses.1.street_2');
echo $this->Form->control('addresses.1.zip');
echo $this->Form->control('addresses.1.city');
echo $this->Form->control('addresses.1.state');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
Now that we have the form, we need to convert the request data. The Table class provides an easy and efficient way to convert one or many entities from request data. It’s needed to define which associations should be marshalled, using associated
.
public function add()
{
$user = $this->Users->newEmptyEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->getData(), ['associated' => ['Addresses']]);
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
$this->set(compact('user'));
}
In this example we are saving one user and two addresses for the given user.
Associated data is validated by default, If you wish to bypass data validation, pass the validate => false
option, for example: $this->Users->patchEntity($user, $this->request->getData(), ['associated' => ['Addresses' => [‘validate’ => false]]])
.
We are all about working smarter and in less time, so I hope this information will be useful! Take a look here for more information: https://book.cakephp.org/4/en/orm/saving-data.html#converting-request-data-into-entities
We will be posting more Common Errors while using CakePHP. Keep checking back!
Written by Amanda on August 17, 2020 •
6228 VIEWS
There we were… just 8 months ago, bright eyed and planning our annual CakeFest event for 2020. The community picked Los Angeles for a host city, and who doesn’t love LA? Our team was excited! Now, fast forward, and we are planning our virtual event. After sitting in on a few well executed virtual events this year, I was pleasantly surprised and impressed. There are still ways to keep these conferences interactive, but theres no denying that it is hard to beat the face to face communication within our CakePHP community, like we have experienced in the past. Wondering what our (and many others) event will look like this year? Let’s see….
Written by Ajibarra on August 10, 2020 •
5724 VIEWS
CakePHP is a web development framework running on PHP. CakePHP provides tools to help build websites and web apps faster, stable and very easy to maintain. We will outline some of the interesting features of the CakePHP framework below:
Written by Larry on July 27, 2020 •
5247 VIEWS
A worldwide pandemic is not something a company, a manager, or a team ever plans for. This time 6 months ago we were bringing in more clients than anticipated, and planning for an international conference. Fast forward to now, just like most companies we have been hit, our conference has gone virtual, and many employees are still worried about what is to come. Here are 5 things I have learned during these uncertain times: